SuperAPI AI 工具集

error management

编程与开发

查看项目

更新于 10-16 10:44

技能介绍

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”:错误哲学