Effect 中的错误管理
概述
Effect 区分两类失败:
- 期望错误(可恢复):表示在 Error 类型参数中,在编译期被追踪。
- 缺陷(意外/不可恢复):运行时异常、bug,不在类型签名中。
Effect<Success, Error, Requirements>;
// ^^^^^ 期望错误位于此处
创建类型化错误
使用 Schema.TaggedError(推荐)
import { Schema, Effect } from "effect";
class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", { userId: Schema.String }) {}
// 注意:Schema.Unknown 在这里语义正确,因为 `cause` 捕获的是任意被捕获的异常,
// 其类型在领域层面确实未知。这不是类型弱化——JavaScript 异常可以是任意值。
class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", { cause: Schema.Unknown }) {}
const getUser = (id: string): Effect.Effect<User, UserNotFound | NetworkError> =>
Effect.gen(function* () {
// ...实现
return yield* Effect.fail(new UserNotFound({ userId: id }));
});
使用 Effect.fail
const divide = (a: number, b: number) => (b === 0 ? Effect.fail(new DivisionByZero()) : Effect.succeed(a / b));
捕获与恢复错误
catchAll - 捕获所有错误
program.pipe(Effect.catchAll((error) => Effect.succeed("fallback value")));
catchTag - 按标签捕获特定错误
const program = getUser(id).pipe(
Effect.catchTag("UserNotFound", (error) => Effect.succeed(defaultUser)),
Effect.catchTag("NetworkError", (error) => Effect.retry(Schedule.exponential("1 second"))),
);
catchTags - 处理多种错误类型
const program = getUser(id).pipe(
Effect.catchTags({
UserNotFound: (error) => Effect.succeed(defaultUser),
NetworkError: (error) => Effect.fail(new ServiceUnavailable()),
}),
);
orElse - 提供回退 Effect
const primary = fetchFromPrimary();
const fallback = fetchFromBackup();
const resilient = primary.pipe(Effect.orElse(() => fallback));
orElseSucceed - 提供回退值
const program = fetchConfig().pipe(Effect.orElseSucceed(() => defaultConfig));
转换错误
mapError - 转换错误类型
const program = rawApiCall().pipe(Effect.mapError((error) => new ApiError({ cause: error })));
mapBoth - 同时转换成功与错误
const program = effect.pipe(
Effect.mapBoth({
onError: (e) => new WrappedError({ cause: e }),
onSuccess: (a) => a.toUpperCase(),
}),
);
错误累积
在运行多个 effect 时,收集所有错误而不是快速失败:
使用 Effect.all 且 mode: "either"
const results = yield * Effect.all([effect1, effect2, effect3], { mode: "either" });
使用 Effect.partition
const [failures, successes] = yield * Effect.partition(items, (item) => processItem(item));
使用 Effect.validate
const result = yield * Effect.validate([check1, check2, check3], { concurrency: "unbounded" });
缺陷(意外错误)
缺陷是未被类型追踪的 bug/意外失败:
const defect = Effect.die(new Error("Unexpected!"));
const program = effect.pipe(Effect.orDie);
const sandboxed = Effect.sandbox(program);
Cause - 完整错误信息
Cause 类型包含完整的失败信息:
import { Cause, Match } from "effect";
// 在 sandbox 中,你会得到完整的 Cause——使用 Match 处理
const handled = Effect.sandbox(program).pipe(
Effect.catchAll((cause) =>
Match.value(cause).pipe(
Match.when(Cause.isFailure, () => {
// 期望错误
return Effect.succeed(fallback);
}),
Match.when(Cause.isDie, () => {
// 缺陷——记录并恢复
return Effect.succeed(fallback);
}),
Match.when(Cause.isInterrupt, () => {
// 中断
return Effect.succeed(fallback);
}),
Match.orElse(() => Effect.succeed(fallback)),
),
),
);
重试
import { Schedule } from "effect";
const resilient = effect.pipe(
Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.jittered, Schedule.compose(Schedule.recurs(5)))),
);
// 带条件的重试——使用 Match.tag 进行错误类型检查
const conditional = effect.pipe(
Effect.retry({
schedule: Schedule.recurs(3),
while: (error) =>
Match.value(error).pipe(
Match.tag("NetworkError", () => true),
Match.orElse(() => false),
),
}),
);
超时
const withTimeout = effect.pipe(Effect.timeout("5 seconds"));
const failOnTimeout = effect.pipe(
Effect.timeoutFail({
duration: "5 seconds",
onTimeout: () => new TimeoutError(),
}),
);
错误匹配模式
使用 Effect.match
const result =
yield *
effect.pipe(
Effect.match({
onFailure: (error) => `Failed: ${error.message}`,
onSuccess: (value) => `Success: ${value}`,
}),
);
使用 Effect.matchEffect
const result =
yield *
effect.pipe(
Effect.matchEffect({
onFailure: (error) => logError(error).pipe(Effect.as("failed")),
onSuccess: (value) => logSuccess(value).pipe(Effect.as("success")),
}),
);
最佳实践
- 对所有领域错误使用 TaggedError——可启用 catchTag 模式匹配。
- 将错误通道保留给可恢复错误——bug 使用缺陷。
- 在边界处转换错误——将低层错误映射为领域错误。
- 慷慨地使用类型化错误——编译器会免费追踪它们。
- 累积验证错误——验证时不要快速失败。
- 只对真正无类型的值使用 Schema.Unknown——错误类型上的 cause 字段是典型示例(被捕获的 JS 异常可以是任意值)。绝不要为你能够描述形状的字段使用 Schema.Unknown 或 Schema.Any——应改为定义合适的 schema。
附加资源
关于全面的错误管理文档,请查阅 ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt。
搜索以下章节:
- “Expected Errors”:创建类型化错误
- “Error Accumulation”:收集多个错误
- “Sandboxing”:处理缺陷
- “Retrying”:重试策略
- “Timing Out”:超时模式
- “Two Types of Errors”:错误哲学