TypeScript Effect系统错误处理实战:函数式错误管理的5个核心模式
2026年,TypeScript 生态的函数式编程范式正在重塑错误处理的方式。Effect-TS 作为 TypeScript 最强大的 Effect 系统,通过类型安全的错误组合、标记错误和声明式错误恢复,让"不可能忘记处理错误"从理想变为现实。相比传统的 try-catch 和 Promise.catch,Effect 系统将错误类型编码到函数签名中,编译器强制你处理每一种可能的错误。本文将从实战角度出发,深入剖析 TypeScript Effect 系统错误处理的5个核心模式,帮助你构建健壮的生产级应用。
核心概念
| 概念 | 说明 | 适用场景 |
|---|---|---|
| Effect | 描述计算的惰性值,可包含错误 | 所有副作用操作 |
| Effect.Success | 成功的Effect | 纯计算 |
| Effect.Failure | 失败的Effect | 错误传播 |
| Tagged Error | 带标签的错误类型 | 错误分类与匹配 |
| Layer | 依赖注入层 | 服务依赖管理 |
| Schedule | 重试策略调度器 | 错误恢复 |
| Cause | 错误原因追踪 | 错误诊断 |
问题分析:5大痛点
- 错误类型丢失:Promise 的 catch 只能拿到 unknown 类型,运行时才知道错误是什么,编译器无法帮你检查未处理的错误,线上经常出现"undefined is not a function"
- 错误处理不一致:团队中有人用 try-catch,有人用 .catch(),有人用 Result 模式,有人直接 throw,错误处理风格混乱,新人完全不知道该怎么写
- 错误恢复困难:网络请求失败需要重试,数据库连接断开需要切换备库,但 try-catch 嵌套3层后代码已经不可读了
- 错误组合爆炸:函数A可能抛出错误E1,函数B可能抛出错误E2,组合后错误类型是 E1|E2,再组合就变成 E1|E2|E3|E4...类型推导完全失控
- 错误日志缺失上下文:catch 到错误后只记录了 error.message,缺少调用链、参数、时间等上下文,线上排障时"错误信息看不懂,堆栈追踪找不到根因"
模式一:Effect-TS基础与类型安全错误
Effect 的核心思想是:错误是类型系统的一部分,不是运行时的意外。
// src/effects/basic.ts - Effect基础
import { Effect } from "effect";
// ====== 传统方式 vs Effect方式 ======
// ❌ 传统方式:错误类型丢失
async function fetchUserTraditional(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
// 调用者不知道这个函数可能抛出错误!
}
return response.json();
}
// ✅ Effect方式:错误编码在类型中
class FetchError {
readonly _tag = "FetchError";
constructor(readonly status: number, readonly url: string) {}
}
class ParseError {
readonly _tag = "ParseError";
constructor(readonly message: string, readonly raw: unknown) {}
}
const fetchUser = (id: string): Effect.Effect<User, FetchError | ParseError, never> =>
Effect.gen(function* (_) {
const url = `/api/users/${id}`;
const response = yield* _(
Effect.tryPromise({
try: () => fetch(url),
catch: (error) => new FetchError(0, url), // 网络错误
})
);
if (!response.ok) {
yield* _(Effect.fail(new FetchError(response.status, url)));
}
const data = yield* _(
Effect.try({
try: () => response.json() as Promise<User>,
catch: (error) => new ParseError("Failed to parse JSON", error),
})
);
return data;
});
// 类型签名明确告诉你:这个Effect可能失败,错误类型是 FetchError | ParseError
// 编译器会强制你处理这些错误!
// ====== 基础Effect操作 ======
// 成功值
const success: Effect.Effect<number, never, never> = Effect.succeed(42);
// 失败值
const failure: Effect.Effect<never, string, never> = Effect.fail("something went wrong");
// 从同步计算创建
const fromSync = Effect.sync(() => {
const value = Math.random();
return value > 0.5 ? value : -1;
});
// 从Promise创建
const fromPromise = Effect.tryPromise({
try: () => fetch("https://api.toolsku.com/health"),
catch: (error) => new FetchError(0, "https://api.toolsku.com/health"),
});
// ====== 运行Effect ======
import { Runtime } from "effect";
const program = fetchUser("user-123");
// 方式1:使用默认Runtime运行
const result1 = await Effect.runPromise(program);
// 返回 User,但如果失败会抛出未处理的错误
// 方式2:安全运行,获取Exit
const result2 = await Effect.runPromiseExit(program);
// Exit<User, FetchError | ParseError>
// 成功: Exit.Success<User>
// 失败: Exit.Failure<FetchError | ParseError>
if (result2._tag === "Success") {
console.log("User:", result2.value);
} else {
console.log("Error:", result2.cause);
}
// 方式3:使用自定义Runtime(推荐生产使用)
interface AppEnv {
readonly apiBaseUrl: string;
readonly timeout: number;
}
const AppLive = Runtime.defaultRuntime;
const customProgram = Effect.gen(function* (_) {
const env = yield* _(Effect.service(AppEnv));
const url = `${env.apiBaseUrl}/users/123`;
// ...
});
// src/effects/user-service.ts - 完整的用户服务Effect
import { Effect, Context, Layer } from "effect";
// ====== 定义错误类型 ======
class UserNotFoundError {
readonly _tag = "UserNotFoundError";
constructor(readonly userId: string) {}
}
class DatabaseError {
readonly _tag = "DatabaseError";
constructor(
readonly operation: string,
readonly cause: unknown,
) {}
}
class ValidationError {
readonly _tag = "ValidationError";
constructor(
readonly field: string,
readonly message: string,
) {}
}
// ====== 定义服务接口 ======
interface UserRepository {
readonly findById: (id: string) => Effect.Effect<User, UserNotFoundError | DatabaseError, never>;
readonly create: (data: CreateUserInput) => Effect.Effect<User, ValidationError | DatabaseError, never>;
readonly update: (id: string, data: UpdateUserInput) => Effect.Effect<User, UserNotFoundError | ValidationError | DatabaseError, never>;
readonly delete: (id: string) => Effect.Effect<void, UserNotFoundError | DatabaseError, never>;
}
const UserRepository = Context.GenericTag<UserRepository>("UserRepository");
// ====== 实现业务逻辑 ======
const getUser = (id: string): Effect.Effect<User, UserNotFoundError | DatabaseError, UserRepository> =>
Effect.gen(function* (_) {
const repo = yield* _(UserRepository);
return yield* _(repo.findById(id));
});
const createUser = (input: CreateUserInput): Effect.Effect<User, ValidationError | DatabaseError, UserRepository> =>
Effect.gen(function* (_) {
// 验证输入
yield* _(validateCreateUserInput(input));
const repo = yield* _(UserRepository);
return yield* _(repo.create(input));
});
const validateCreateUserInput = (input: CreateUserInput): Effect.Effect<void, ValidationError, never> =>
Effect.gen(function* (_) {
if (!input.email || !input.email.includes("@")) {
yield* _(Effect.fail(new ValidationError("email", "Invalid email format")));
}
if (!input.name || input.name.length < 2) {
yield* _(Effect.fail(new ValidationError("name", "Name must be at least 2 characters")));
}
if (input.age !== undefined && (input.age < 0 || input.age > 150)) {
yield* _(Effect.fail(new ValidationError("age", "Age must be between 0 and 150")));
}
});
// ====== 类型定义 ======
interface User {
id: string;
name: string;
email: string;
age: number;
createdAt: string;
updatedAt: string;
}
interface CreateUserInput {
name: string;
email: string;
age?: number;
}
interface UpdateUserInput {
name?: string;
email?: string;
age?: number;
}
模式二:标记错误(Tagged Error)与错误分类
标记错误是 Effect 系统的灵魂。通过 _tag 字段,你可以在类型层面精确匹配和处理不同类型的错误。
// src/effects/tagged-errors.ts - 标记错误与分类
import { Effect, Match } from "effect";
// ====== 定义领域错误层次 ======
// 基础错误接口
interface DomainError {
readonly _tag: string;
readonly timestamp: string;
readonly traceId: string;
}
// 认证错误
class UnauthorizedError implements DomainError {
readonly _tag = "UnauthorizedError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly reason: "expired_token" | "invalid_token" | "no_token",
) {}
}
class ForbiddenError implements DomainError {
readonly _tag = "ForbiddenError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly resource: string,
readonly action: string,
) {}
}
// 业务错误
class InsufficientBalanceError implements DomainError {
readonly _tag = "InsufficientBalanceError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly required: number,
readonly available: number,
) {}
}
class DuplicateResourceError implements DomainError {
readonly _tag = "DuplicateResourceError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly resourceType: string,
readonly identifier: string,
) {}
}
// 基础设施错误
class DatabaseConnectionError implements DomainError {
readonly _tag = "DatabaseConnectionError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly host: string,
readonly cause: unknown,
) {}
}
class CacheError implements DomainError {
readonly _tag = "CacheError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly operation: string,
readonly cause: unknown,
) {}
}
// ====== 使用Match进行类型安全的错误处理 ======
type AppError =
| UnauthorizedError
| ForbiddenError
| InsufficientBalanceError
| DuplicateResourceError
| DatabaseConnectionError
| CacheError;
const handleAppError = (error: AppError): HttpResponse => {
return Match.value(error).pipe(
Match.tag("UnauthorizedError", (err) => ({
status: 401,
body: {
code: "UNAUTHORIZED",
message: `Authentication failed: ${err.reason}`,
traceId: err.traceId,
},
})),
Match.tag("ForbiddenError", (err) => ({
status: 403,
body: {
code: "FORBIDDEN",
message: `Cannot ${err.action} on ${err.resource}`,
traceId: err.traceId,
},
})),
Match.tag("InsufficientBalanceError", (err) => ({
status: 409,
body: {
code: "INSUFFICIENT_BALANCE",
message: `Required ${err.required}, available ${err.available}`,
traceId: err.traceId,
},
})),
Match.tag("DuplicateResourceError", (err) => ({
status: 409,
body: {
code: "DUPLICATE_RESOURCE",
message: `${err.resourceType} '${err.identifier}' already exists`,
traceId: err.traceId,
},
})),
Match.tag("DatabaseConnectionError", (err) => ({
status: 503,
body: {
code: "SERVICE_UNAVAILABLE",
message: "Database temporarily unavailable",
traceId: err.traceId,
},
})),
Match.tag("CacheError", (err) => ({
status: 503,
body: {
code: "CACHE_ERROR",
message: "Cache service error",
traceId: err.traceId,
},
})),
Match.exhaustive, // 编译器保证处理了所有错误类型!
);
};
// ====== 错误分类工具 ======
const isRetryable = (error: AppError): boolean => {
switch (error._tag) {
case "DatabaseConnectionError":
case "CacheError":
return true; // 基础设施错误可重试
case "UnauthorizedError":
case "ForbiddenError":
case "InsufficientBalanceError":
case "DuplicateResourceError":
return false; // 业务错误不应重试
}
};
const isErrorSeverity = (error: AppError): "low" | "medium" | "high" | "critical" => {
switch (error._tag) {
case "CacheError":
return "low";
case "UnauthorizedError":
case "DuplicateResourceError":
return "medium";
case "ForbiddenError":
case "InsufficientBalanceError":
return "high";
case "DatabaseConnectionError":
return "critical";
}
};
模式三:错误恢复与重试策略
Effect 的 Schedule 系统提供了声明式的重试策略,让错误恢复代码既强大又可读。
// src/effects/recovery.ts - 错误恢复与重试
import { Effect, Schedule, Duration } from "effect";
// ====== 基础重试策略 ======
// 简单重试3次
const withSimpleRetry = <A, E, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(Effect.retry(Schedule.recurs(3)));
// 指数退避重试
const withExponentialBackoff = <A, E, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.retry(
Schedule.exponential(Duration.millis(100), 2.0).pipe(
Schedule.jittered, // 添加随机抖动
Schedule.whileOutput(Duration.lessThan(Duration.seconds(30))), // 最大间隔30秒
Schedule.compose(Schedule.recurs(5)), // 最多5次
)
)
);
// 按错误类型选择重试策略
const withSmartRetry = <A, E extends { readonly _tag: string }, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.retry(
Schedule.whileInput((error: E) => {
// 只重试基础设施错误
return error._tag === "DatabaseConnectionError" || error._tag === "CacheError";
}).pipe(
Schedule.compose(
Schedule.exponential(Duration.millis(200), 2.0).pipe(
Schedule.jittered,
Schedule.compose(Schedule.recurs(5)),
)
)
)
)
);
// ====== 高级重试策略 ======
// 带熔断器的重试
interface CircuitBreakerState {
readonly failureCount: number;
readonly lastFailureTime: number;
readonly state: "closed" | "open" | "half-open";
}
const withCircuitBreaker = <A, E, R>(
effect: Effect.Effect<A, E, R>,
options: {
failureThreshold: number;
resetTimeout: Duration.Duration;
halfOpenMaxAttempts: number;
},
): Effect.Effect<A, E, R> =>
Effect.gen(function* (_) {
const state = yield* _(
Effect.makeRef<CircuitBreakerState>({
failureCount: 0,
lastFailureTime: 0,
state: "closed",
})
);
const updateState = (result: "success" | "failure") =>
state.update((current) => {
const now = Date.now();
if (current.state === "open") {
// 检查是否应该进入半开状态
if (now - current.lastFailureTime > Duration.toMillis(options.resetTimeout)) {
return {
failureCount: result === "failure" ? 1 : 0,
lastFailureTime: now,
state: "half-open" as const,
};
}
return current;
}
if (result === "success") {
return { failureCount: 0, lastFailureTime: 0, state: "closed" as const };
}
const newFailureCount = current.failureCount + 1;
return {
failureCount: newFailureCount,
lastFailureTime: now,
state: newFailureCount >= options.failureThreshold
? "open" as const
: "closed" as const,
};
});
const currentState = yield* _(state.get);
if (currentState.state === "open") {
yield* _(Effect.fail(new Error("Circuit breaker is open") as unknown as E));
}
const result = yield* _(
effect.pipe(
Effect.tap(() => updateState("success")),
Effect.catchAll((error) =>
Effect.gen(function* (_) {
yield* _(updateState("failure"));
yield* _(Effect.fail(error));
})
),
)
);
return result;
});
// ====== 降级策略 ======
const withFallback = <A, E1, E2, R1, R2>(
primary: Effect.Effect<A, E1, R1>,
fallback: Effect.Effect<A, E2, R2>,
): Effect.Effect<A, E1 | E2, R1 | R2> =>
Effect.catchAll(primary, () => fallback);
// 带缓存的降级
const withCacheFallback = <A, E, R>(
primary: Effect.Effect<A, E, R>,
cacheKey: string,
): Effect.Effect<A, E, R, R | CacheService> =>
Effect.gen(function* (_) {
const cache = yield* _(CacheService);
// 先尝试主流程
const result = yield* _(
primary.pipe(
// 成功时更新缓存
Effect.tap((value) => cache.set(cacheKey, value, Duration.minutes(10))),
// 失败时尝试缓存
Effect.catchAll(() =>
Effect.gen(function* (_) {
const cached = yield* _(cache.get<A>(cacheKey));
if (cached) {
yield* _(Effect.logWarning(`Using cached value for ${cacheKey}`));
return cached;
}
// 缓存也没有,返回原始错误
return yield* _(primary); // 重新抛出原始错误
})
),
)
);
return result;
});
// ====== 超时与竞速 ======
// 超时控制
const withTimeout = <A, E, R>(
effect: Effect.Effect<A, E, R>,
duration: Duration.Duration,
): Effect.Effect<A, E | TimeoutError, R> =>
effect.pipe(
Effect.timeout(duration),
Effect.mapEffect(
(option) =>
option._tag === "Some"
? Effect.succeed(option.value)
: Effect.fail(new TimeoutError(Duration.toMillis(duration)))
),
);
// 竞速:两个数据源,谁先返回用谁
const race = <A, E1, E2, R1, R2>(
effect1: Effect.Effect<A, E1, R1>,
effect2: Effect.Effect<A, E2, R2>,
): Effect.Effect<A, E1 | E2, R1 | R2> =>
Effect.race(effect1, effect2);
// ====== 服务定义 ======
interface CacheService {
readonly get: <A>(key: string) => Effect.Effect<A | null, never, never>;
readonly set: <A>(key: string, value: A, ttl: Duration.Duration) => Effect.Effect<void, never, never>;
}
const CacheService = Context.GenericTag<CacheService>("CacheService");
class TimeoutError {
readonly _tag = "TimeoutError";
constructor(readonly durationMs: number) {}
}
模式四:错误组合与Layer依赖
当多个 Effect 组合时,错误类型会自动合并。Layer 系统则管理服务的依赖关系。
// src/effects/composition.ts - 错误组合与Layer
import { Effect, Context, Layer } from "effect";
// ====== 错误组合 ======
// 组合多个Effect,错误类型自动合并
const transferMoney = (
fromUserId: string,
toUserId: string,
amount: number,
): Effect.Effect<
TransferResult,
| UserNotFoundError
| InsufficientBalanceError
| DatabaseError
| UnauthorizedError,
UserRepository | AuditService | NotificationService
> =>
Effect.gen(function* (_) {
const repo = yield* _(UserRepository);
const audit = yield* _(AuditService);
const notification = yield* _(NotificationService);
// 并行获取两个用户
const [fromUser, toUser] = yield* _(
Effect.all([
repo.findById(fromUserId),
repo.findById(toUserId),
])
);
// 检查余额
if (fromUser.balance < amount) {
yield* _(Effect.fail(
new InsufficientBalanceError(generateTraceId(), amount, fromUser.balance)
));
}
// 执行转账(事务)
const result = yield* _(
repo.transfer(fromUserId, toUserId, amount).pipe(
// 记录审计日志(不影响主流程)
Effect.tap((result) =>
audit.logTransfer(fromUserId, toUserId, amount, result.transactionId).pipe(
Effect.catchAll((error) =>
Effect.logError(`Audit log failed: ${error}`)
)
)
),
// 发送通知(不影响主流程)
Effect.tap((result) =>
notification.notifyTransfer(fromUser, toUser, amount, result.transactionId).pipe(
Effect.catchAll((error) =>
Effect.logError(`Notification failed: ${error}`)
)
)
),
)
);
return result;
});
// ====== 错误隔离 ======
// 将内部错误映射为API错误
const transferMoneyApi = (
fromUserId: string,
toUserId: string,
amount: number,
): Effect.Effect<TransferResult, ApiError, UserRepository | AuditService | NotificationService> =>
transferMoney(fromUserId, toUserId, amount).pipe(
Effect.catchAll((error): Effect.Effect<never, ApiError, never> => {
switch (error._tag) {
case "UserNotFoundError":
return Effect.fail(new ApiError(404, "USER_NOT_FOUND", error.userId));
case "InsufficientBalanceError":
return Effect.fail(new ApiError(409, "INSUFFICIENT_BALANCE", `Required ${error.required}`));
case "DatabaseError":
return Effect.fail(new ApiError(503, "SERVICE_UNAVAILABLE", "Database error"));
case "UnauthorizedError":
return Effect.fail(new ApiError(401, "UNAUTHORIZED", error.reason));
}
})
);
// ====== Layer依赖管理 ======
// 定义服务标签
const AuditService = Context.GenericTag<AuditService>("AuditService");
const NotificationService = Context.GenericTag<NotificationService>("NotificationService");
// 实现Layer
const AuditServiceLive = Layer.succeed(AuditService, {
logTransfer: (from, to, amount, txId) =>
Effect.gen(function* (_) {
yield* _(Effect.logInfo(`Audit: ${from} -> ${to}, amount: ${amount}, txId: ${txId}`));
}),
});
const NotificationServiceLive = Layer.succeed(NotificationService, {
notifyTransfer: (from, to, amount, txId) =>
Effect.gen(function* (_) {
yield* _(Effect.logInfo(`Notify: transfer ${amount} from ${from.name} to ${to.name}`));
}),
});
// 组合Layer
const AppLayer = Layer.mergeAll(
AuditServiceLive,
NotificationServiceLive,
);
// ====== 使用Layer运行程序 ======
const program = transferMoneyApi("user-1", "user-2", 100);
const main = program.pipe(
Effect.provide(AppLayer),
Effect.catchAll((error) =>
Effect.gen(function* (_) {
yield* _(Effect.logError(`API Error: ${error.code} - ${error.message}`));
return yield* _(Effect.fail(error));
})
),
);
// 运行
const result = await Effect.runPromise(main);
模式五:生产级错误管理与日志集成
在生产环境中,错误管理需要与日志、监控、告警系统深度集成。
// src/effects/production.ts - 生产级错误管理
import { Effect, Fiber, Metric, Schedule, Duration } from "effect";
// ====== 错误日志中间件 ======
const withErrorLogging = <A, E extends { readonly _tag: string }, R>(
effect: Effect.Effect<A, E, R>,
context: Record<string, unknown> = {},
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.tapError((error) =>
Effect.gen(function* (_) {
yield* _(Effect.logError("Operation failed", {
errorTag: error._tag,
error: JSON.stringify(error, null, 2),
context,
timestamp: new Date().toISOString(),
fiberId: Fiber.currentFiberId(),
}));
})
),
);
// ====== 错误指标收集 ======
const errorCounter = Metric.counter("app_errors_total");
const errorRateByType = Metric.keyValue("app_errors_by_type", Metric.counter);
const withErrorMetrics = <A, E extends { readonly _tag: string }, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.tapError((error) =>
Effect.gen(function* (_) {
yield* _(Metric.increment(errorCounter));
yield* _(Metric.increment(errorRateByType(error._tag)));
})
),
);
// ====== 错误报告 ======
interface ErrorReport {
readonly id: string;
readonly errorTag: string;
readonly message: string;
readonly stack?: string;
readonly context: Record<string, unknown>;
readonly timestamp: string;
readonly traceId: string;
readonly severity: "low" | "medium" | "high" | "critical";
readonly retryable: boolean;
}
const createErrorReport = <E extends DomainError>(
error: E,
context: Record<string, unknown> = {},
): ErrorReport => ({
id: crypto.randomUUID(),
errorTag: error._tag,
message: JSON.stringify(error),
context,
timestamp: error.timestamp,
traceId: error.traceId,
severity: isErrorSeverity(error),
retryable: isRetryable(error),
});
// ====== 全局错误处理器 ======
const globalErrorHandler = <A, E extends DomainError>(
effect: Effect.Effect<A, E, never>,
): Effect.Effect<A, never, never> =>
effect.pipe(
// 1. 添加日志
withErrorLogging,
// 2. 添加指标
withErrorMetrics,
// 3. 生成错误报告
Effect.catchAll((error: E) =>
Effect.gen(function* (_) {
const report = createErrorReport(error);
// 发送到错误追踪服务(如Sentry)
yield* _(
Effect.tryPromise({
try: () => fetch("https://sentry.io/api/12345/store/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event_id: report.id,
message: `[${report.errorTag}] ${report.message}`,
extra: report.context,
tags: {
errorTag: report.errorTag,
severity: report.severity,
retryable: String(report.retryable),
},
timestamp: report.timestamp,
}),
}),
catch: () => new Error("Failed to report to Sentry"),
}).pipe(
Effect.catchAll(() => Effect.logWarning("Failed to report error to Sentry")),
)
);
// 根据严重程度决定是否告警
if (report.severity === "critical") {
yield* _(sendAlert(report));
}
// 返回降级响应
return yield* _(Effect.succeed(getFallbackResponse(error) as A));
})
),
);
// ====== 错误边界 ======
const errorBoundary = <A, E extends DomainError, R>(
effect: Effect.Effect<A, E, R>,
options: {
readonly fallback?: A;
readonly maxRetries?: number;
readonly retryDelay?: Duration.Duration;
readonly onError?: (error: E) => void;
},
): Effect.Effect<A, E, R> =>
effect.pipe(
// 重试
Effect.retry(
Schedule.exponential(options.retryDelay ?? Duration.millis(100), 2.0).pipe(
Schedule.jittered,
Schedule.whileInput((error: E) => isRetryable(error)),
Schedule.compose(Schedule.recurs(options.maxRetries ?? 3)),
)
),
// 降级
options.fallback
? Effect.catchAll(() => Effect.succeed(options.fallback!))
: Effect.catchAll((error) => {
options.onError?.(error);
return Effect.fail(error);
}),
);
// ====== 辅助函数 ======
function generateTraceId(): string {
return `trace-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
const sendAlert = (report: ErrorReport): Effect.Effect<void, never, never> =>
Effect.logCritical(`🚨 CRITICAL ALERT: ${report.errorTag} - ${report.message}`);
function getFallbackResponse(error: DomainError): unknown {
return { status: "degraded", errorTag: error._tag };
}
// ====== HTTP处理器集成 ======
const makeHttpHandler = <A, E extends DomainError, R>(
effect: Effect.Effect<A, E, R>,
) => {
return async (request: Request): Promise<Response> => {
const program = effect.pipe(
withErrorLogging({ path: new URL(request.url).pathname }),
withErrorMetrics,
Effect.map((data) => Response.json({ success: true, data })),
Effect.catchAll((error: E) =>
Effect.succeed(
Response.json(
{ success: false, error: createErrorReport(error) },
{ status: getHttpStatus(error) }
)
)
),
);
return Effect.runPromise(program);
};
};
function getHttpStatus(error: DomainError): number {
switch (error._tag) {
case "UnauthorizedError": return 401;
case "ForbiddenError": return 403;
case "UserNotFoundError": return 404;
case "InsufficientBalanceError":
case "DuplicateResourceError": return 409;
case "ValidationError": return 422;
case "DatabaseConnectionError":
case "CacheError": return 503;
default: return 500;
}
}
踩坑指南
坑1:忘记运行Effect
// ❌ 错误:Effect是惰性的,不运行什么都不会发生
const fetchUser = (id: string) =>
Effect.gen(function* (_) {
const response = yield* _(Effect.tryPromise(() => fetch(`/api/users/${id}`)));
return response;
});
fetchUser("123"); // 什么都不会发生!
// ✅ 正确:必须显式运行Effect
const program = fetchUser("123");
const result = await Effect.runPromise(program);
坑2:在Effect中使用throw
// ❌ 错误:在Effect中使用throw会绕过错误类型系统
const parseJson = (input: string): Effect.Effect<unknown, never, never> =>
Effect.sync(() => JSON.parse(input)); // throw会变成Defect,不在类型中!
// ✅ 正确:使用Effect.try或Effect.fail
const parseJson = (input: string): Effect.Effect<unknown, ParseError, never> =>
Effect.try({
try: () => JSON.parse(input),
catch: (error) => new ParseError("JSON parse failed", error),
});
坑3:错误类型不匹配导致catchAll失败
// ❌ 错误:catchAll需要处理所有错误类型
const program = Effect.gen(function* (_) {
const user = yield* _(fetchUser("123")); // 错误: FetchError | ParseError
}).pipe(
Effect.catchAll((error: FetchError) => // ❌ 没有处理ParseError!
Effect.succeed(null)
)
);
// ✅ 正确:使用Match处理所有错误类型
const program = Effect.gen(function* (_) {
const user = yield* _(fetchUser("123"));
}).pipe(
Effect.catchAll((error: FetchError | ParseError) =>
Match.value(error).pipe(
Match.tag("FetchError", () => Effect.succeed(null)),
Match.tag("ParseError", () => Effect.succeed(null)),
Match.exhaustive,
)
)
);
坑4:Layer未提供导致运行时错误
// ❌ 错误:使用了UserRepository但未提供Layer
const program = getUser("123");
await Effect.runPromise(program); // 运行时错误:Missing service: UserRepository
// ✅ 正确:提供Layer后再运行
const program = getUser("123").pipe(
Effect.provide(UserRepositoryLive),
);
await Effect.runPromise(program);
坑5:在gen函数中忘记yield*
// ❌ 错误:忘记yield*,Effect不会执行
const program = Effect.gen(function* (_) {
const user = fetchUser("123"); // ❌ 缺少yield* _
return user; // 返回的是Effect,不是User!
});
// ✅ 正确:使用yield* _()解包Effect
const program = Effect.gen(function* (_) {
const user = yield* _(fetchUser("123")); // ✅ 正确解包
return user;
});
错误排查表
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Missing service: X |
Layer未提供 | 使用Effect.provide()提供对应Layer |
Defect: Uncaught error |
Effect中使用了throw | 改用Effect.try()或Effect.fail() |
Timeout after X ms |
Effect执行超时 | 检查死循环,增大timeout或添加重试 |
Type error: Property '_tag' is missing |
错误类型缺少_tag字段 | 为错误类添加readonly _tag属性 |
Non-exhaustive match |
Match未覆盖所有情况 | 添加Match.exhaustive确保完整性 |
Fiber interrupted |
Fiber被取消 | 检查是否有超时或手动中断 |
Maximum call stack exceeded |
无限递归 | 检查递归Effect是否有终止条件 |
Cannot read property of undefined |
Effect未运行 | 确保使用Effect.runPromise()运行 |
Schedule limit exceeded |
重试次数耗尽 | 检查重试策略,增大重试次数或修复根因 |
Layer cycle detected |
Layer循环依赖 | 重构Layer依赖关系,消除循环 |
进阶优化
-
Schema验证集成:使用
@effect/schema在Effect管道入口处自动验证输入数据,验证失败自动生成类型安全的ValidationError,减少80%的手动验证代码 -
分布式追踪集成:通过自定义Layer集成OpenTelemetry,每个Effect自动携带traceId和spanId,实现端到端的分布式追踪,排障时间从小时级降至分钟级
-
错误聚合与去重:开发错误聚合中间件,相同错误在时间窗口内只报告一次,减少90%的告警噪音,同时保留错误频率统计
-
自适应重试策略:基于历史错误率和响应时间的自适应重试算法,自动调整退避参数和重试次数,在错误率飙升时主动降低重试频率保护下游
-
错误预算管理:实现SLO错误预算系统,当错误预算耗尽时自动触发流量切换或降级,保护核心业务可用性
方案对比
| 维度 | Effect-TS | try-catch | neverthrow | ts-results |
|---|---|---|---|---|
| 类型安全 | ✅ 完全编码 | ❌ 运行时 | ✅ Result类型 | ✅ Result类型 |
| 错误组合 | ✅ 自动合并 | ❌ 手动 | ⚠️ 需手动 | ⚠️ 需手动 |
| 重试策略 | ✅ Schedule | ❌ 手动 | ❌ 无 | ❌ 无 |
| 依赖注入 | ✅ Layer | ❌ 无 | ❌ 无 | ❌ 无 |
| 并发控制 | ✅ Fiber | ❌ 无 | ❌ 无 | ❌ 无 |
| 学习曲线 | 陡峭 | 平缓 | 中等 | 中等 |
| 包大小 | ~50KB | 0 | ~5KB | ~3KB |
| 生态集成 | 丰富 | 原生 | 有限 | 有限 |
| 适合项目 | 大型应用 | 小型脚本 | 中型项目 | 中型项目 |
💡 选择建议:如果项目超过10万行代码且需要完整的错误管理体系,Effect-TS是最佳选择;如果追求简单且项目较小,neverthrow足够;如果只是简单脚本,try-catch就好。
总结
错误不是异常,而是计算的一部分。Effect系统将这个理念贯彻到类型系统的每一个角落——错误类型编码在函数签名中,编译器强制你处理每一种可能的失败,声明式的重试和降级让恢复逻辑既强大又可读。掌握这5个核心模式——Effect基础与类型安全错误、标记错误与分类、错误恢复与重试、错误组合与Layer依赖、生产级错误管理——你就拥有了构建"不可能出错"的TypeScript应用的完整方法论。
在线工具推荐
本站提供浏览器本地工具,免注册即可试用 →