用Effect故事化错误处理:TypeScript大型应用的“超新星“级可靠性方案

📅 2026/7/25 3:38:55
用Effect故事化错误处理:TypeScript大型应用的“超新星“级可靠性方案
用Effect故事化错误处理TypeScript大型应用的超新星级可靠性方案传统错误处理的痛点try-catch散落各处TypeScript项目的错误处理通常是这样的// 场景用户注册流程 async function registerUser(email: string, password: string) { try { // 1. 验证邮箱格式 if (!isValidEmail(email)) { throw new Error(Invalid email); } // 2. 检查用户是否已存在 const existing await db.users.findByEmail(email); if (existing) { throw new Error(Email already exists); } // 3. 创建用户 const user await db.users.create({ email, password: hash(password) }); // 4. 发送欢迎邮件 await sendWelcomeEmail(user); return user; } catch (error) { // 问题这里捕获的错误可能是任何类型 console.error(Registration failed:, error); throw error; // 重新抛出但调用者不知道会抛出什么错误 } }问题错误信息不结构化Error对象只有message没有错误码、错误上下文错误类型不明确调用者不知道registerUser会抛出哪些类型的错误错误处理分散每个函数都要写try-catch代码冗余Effect的解决方案类型化的错误通道Effect原Effect-TS是一个函数式编程库它把错误作为类型系统的一等公民。核心概念EffectSuccess, Error, Requirements每个Effect代表一个可能失败的操作它的类型签名明确标注Success成功时的返回类型Error失败时的错误类型可以是联合类型如EmailError | DbErrorRequirements这个操作需要什么依赖如Database、EmailService重写上面的注册流程import { Effect, pipe } from effect; // 定义结构化错误类型 class InvalidEmailError { readonly _tag InvalidEmailError; constructor(readonly email: string) {} } class EmailAlreadyExistsError { readonly _tag EmailAlreadyExistsError; constructor(readonly email: string) {} } class DbError { readonly _tag DbError; constructor(readonly cause: unknown) {} } class EmailSendError { readonly _tag EmailSendError; constructor(readonly cause: unknown) {} } // 定义依赖服务接口 interface Database { readonly findByEmail: (email: string) Effect.EffectUser | null, DbError; readonly create: (data: UserData) Effect.EffectUser, DbError; } interface EmailService { readonly sendWelcome: (user: User) Effect.Effectvoid, EmailSendError; } // 注册流程类型安全的错误处理 const registerUser ( email: string, password: string ): Effect.Effect User, InvalidEmailError | EmailAlreadyExistsError | DbError | EmailSendError, Database EmailService // 需要的依赖 pipe( // 1. 验证邮箱 Effect.sync(() isValidEmail(email)), Effect.filterOrFail(() new InvalidEmailError(email)), // 2. 检查用户是否存在 Effect.flatMap(() Effect.request(Database).flatMap(db db.findByEmail(email))), Effect.flatMap(existing existing ? Effect.fail(new EmailAlreadyExistsError(email)) : Effect.succeed(undefined) ), // 3. 创建用户 Effect.flatMap(() Effect.request(Database).flatMap(db db.create({ email, password: hash(password) }) ) ), // 4. 发送欢迎邮件 Effect.flatMap(user Effect.request(EmailService).flatMap(emailService emailService.sendWelcome(user).pipe( Effect.map(() user) ) ) ) );关键优势错误类型是明确的registerUser的返回类型是EffectUser, A | B | C | D, R——调用者一眼就知道会失败且失败有4种类型错误是结构化的每个错误都是一个类包含上下文如InvalidEmailError包含email字段无try-catch错误通过类型系统传播不需要显式捕获错误恢复与重试让系统自动从故障中恢复Effect提供了声明式的错误恢复策略。重试策略Retry Policyimport { Effect, Schedule } from effect; // 场景调用第三方API可能网络不稳定 const callExternalAPI (): Effect.EffectData, APIError, never { // ... 实现 }; // 重试策略最多重试3次每次间隔指数退避100ms, 200ms, 400ms const retryPolicy Schedule.exponential(100).pipe(Schedule.compose(Schedule.recurs(3))); const callWithRetry pipe( callExternalAPI(), Effect.retry(retryPolicy) );降级策略Fallback// 场景主数据库挂了切换到备用数据库 const queryWithFallback pipe( queryPrimaryDB(), // 可能失败DbError Effect.catchError(error // 如果主库失败查询备用库 queryReplicaDB().pipe( Effect.map(data ({ ...data, source: replica as const })) ) ) );错误日志与告警const registerUserWithLogging pipe( registerUser(email, password), Effect.tapError(error { // 记录错误日志 console.error([Registration Error] ${error._tag}:, error); // 如果是严重错误发送告警 if (error._tag DbError) { return sendAlert(Database connection failed); } return Effect.succeed(undefined); }) );依赖注入让测试和维护变简单Effect的依赖系统让你能声明依赖然后在运行时提供实现——类似React的Context但是类型安全的。声明依赖// 定义服务接口Tag import { Effect, Context } from effect; class Database extends Context.Tag(Database) Database, { query: (sql: string) Effect.Effectunknown, DbError; } () {} // 使用依赖 const getUser (id: string) Effect.request(Database).flatMap(db db.query(SELECT * FROM users WHERE id ${id}) );提供依赖实现// 生产环境实现真实数据库 const DatabaseLive { query: (sql: string) Effect.tryPromise({ try: () pg.query(sql), catch: (cause) new DbError(cause), }), }; // 测试环境实现Mock const DatabaseTest { query: (sql: string) Effect.succeed({ id: 1, name: Test User }), }; // 运行Effect提供依赖 pipe( getUser(1), Effect.provideService(Database, DatabaseLive), // 提供生产实现 Effect.runPromise );测试变得简单// 测试registerUser不需要真实数据库 import { describe, it, expect } from vitest; describe(registerUser, () { it(should fail if email is invalid, async () { const result await pipe( registerUser(invalid-email, password), Effect.provideService(Database, DatabaseTest), Effect.provideService(EmailService, EmailServiceTest), Effect.runPromiseExit // 获取Exit成功或失败 ); expect(result._tag).toBe(Failure); expect((result as any).cause._tag).toBe(InvalidEmailError); }); });实战用Effect重构Next.js API Route场景一个创建订单的API涉及验证用户、检查库存、扣减库存、创建订单、发送确认邮件——任何一步失败整个操作应该回滚。传统实现回调地狱 try-catch// pages/api/orders.ts export async function POST(req: NextRequest) { try { const { userId, productId, quantity } await req.json(); // 1. 验证用户 const user await db.users.findById(userId); if (!user) return NextResponse.json({ error: User not found }, { status: 404 }); // 2. 检查库存 const product await db.products.findById(productId); if (product.stock quantity) { return NextResponse.json({ error: Insufficient stock }, { status: 400 }); } // 3. 扣减库存需要用事务 await db.$transaction(async (tx) { await tx.products.update(productId, { stock: product.stock - quantity }); const order await tx.orders.create({ userId, productId, quantity }); // 4. 发送邮件如果失败事务应该回滚吗 try { await sendOrderConfirmation(order); } catch (emailError) { console.error(Email failed, but order created:, emailError); // 问题订单已创建但邮件没发送——数据不一致 } return NextResponse.json(order); }); } catch (error) { console.error(Order creation failed:, error); return NextResponse.json({ error: Internal Server Error }, { status: 500 }); } }Effect实现类型安全 事务回滚 错误恢复// app/api/orders/route.ts import { Effect, Exit } from effect; import * as Schema from effect/schema/Schema; // 定义错误类型 class UserNotFoundError { readonly _tag UserNotFoundError; constructor(readonly userId: string) {} } class InsufficientStockError { readonly _tag InsufficientStockError; constructor(readonly productId: string) {} } class PaymentError { readonly _tag PaymentError; constructor(readonly cause: unknown) {} } // 定义依赖 interface Database { readonly transaction: A, E, R(effect: Effect.EffectA, E, R) Effect.EffectA, E, R; readonly findUser: (id: string) Effect.EffectUser, UserNotFoundError; readonly findProduct: (id: string) Effect.EffectProduct, never; readonly updateStock: (productId: string, newStock: number) Effect.Effectvoid, never; readonly createOrder: (data: OrderData) Effect.EffectOrder, never; } interface PaymentService { readonly charge: (amount: number) Effect.EffectPaymentResult, PaymentError; } // 创建订单的Effect const createOrder ( userId: string, productId: string, quantity: number ): Effect.Effect Order, UserNotFoundError | InsufficientStockError | PaymentError, Database PaymentService Effect.request(Database).pipe( Effect.flatMap(db // 1. 验证用户 db.findUser(userId).pipe( Effect.flatMap(user // 2. 检查库存 db.findProduct(productId).pipe( Effect.flatMap(product product.stock quantity ? Effect.fail(new InsufficientStockError(productId)) : Effect.succeed({ user, product }) ) ) ) ) ), Effect.flatMap(({ user, product }) // 3. 扣减库存 创建订单在事务里 Effect.request(Database).pipe( Effect.flatMap(db db.transaction( pipe( db.updateStock(productId, product.stock - quantity), Effect.flatMap(() db.createOrder({ userId, productId, quantity, total: product.price * quantity })) ) ) ) ) ), Effect.flatMap(order // 4. 支付如果支付失败事务回滚 Effect.request(PaymentService).pipe( Effect.flatMap(payment payment.charge(order.total)), Effect.map(paymentResult ({ ...order, paymentId: paymentResult.id })) ) ) ); // API Route处理器 export async function POST(req: NextRequest) { const body await req.json(); const result await pipe( createOrder(body.userId, body.productId, body.quantity), Effect.provideService(Database, DatabaseLive), Effect.provideService(PaymentService, PaymentServiceLive), Effect.runPromiseExit ); if (result._tag Success) { return NextResponse.json(result.value); } else { const error result.cause; if (error._tag UserNotFoundError) { return NextResponse.json({ error: User not found }, { status: 404 }); } else if (error._tag InsufficientStockError) { return NextResponse.json({ error: Insufficient stock }, { status: 400 }); } else { return NextResponse.json({ error: Internal Server Error }, { status: 500 }); } } }学习曲线与渐进采用Effect的学习曲线较陡需要理解函数式编程、类型系统、依赖注入等概念。但你可以渐进采用阶段一只在关键业务流程用Effect如支付流程、订单创建流程——这些流程的错误成本高值得用Effect阶段二用Effect的部分模块如只用Effect.tryCatch替代try-catch或只用Effect.retry替代手动重试逻辑阶段三全面采用Effect当团队熟悉后可以把整个应用的错误处理层都用Effect重构结论Effect是TypeScript可靠性的终极方案如果你的应用是玩具项目或原型不需要Effect。但如果你在做真实产品且错误处理不当会导致用户流失或数据损坏Effect是值得投资的。核心价值类型安全的错误编译时就能知道这个函数会失败且失败有X种类型声明式错误恢复重试、降级、回滚都用声明式API而不是嵌套的if-else易于测试依赖注入让Mock所有外部服务变得简单下一步从一个小功能开始试用Effect如发送邮件或调用第三方API——你会发现错误处理从最头疼的事变成最放心的事。