Effect-TS错误处理模式Skill effect-patterns-error-handling

此技能提供Effect-TS中错误处理的三种模式:错误积累、错误传播和自定义错误策略,用于优化错误处理、提升应用健壮性,关键词包括Effect-TS、错误处理、模式、积累、传播、自定义策略、TypeScript、函数式编程。

后端开发 0 次安装 0 次浏览 更新于 3/8/2026

name: effect-patterns-error-handling description: Effect-TS 错误处理模式。在Effect-TS应用中处理错误时使用。

Effect-TS 模式:错误处理

此技能提供了3个精选的Effect-TS错误处理模式。 在处理以下任务相关时使用此技能:

  • 错误处理
  • Effect-TS应用中的最佳实践
  • 现实世界的模式和解决方案

🟡 中级模式

错误处理模式1:积累多个错误

规则: 使用错误积累来一次性报告所有问题,而不是早期失败,对于验证和批处理操作至关重要。

好例子:

此示例演示了错误积累模式。

import { Effect, Data, Cause } from "effect";

interface ValidationError {
  field: string;
  message: string;
  value?: unknown;
}

interface ProcessingResult<T> {
  successes: T[];
  errors: ValidationError[];
}

// 示例1:表单验证与错误积累
const program = Effect.gen(function* () {
  console.log(`
[错误积累] 收集多个错误
`);

  // 表单数据
  interface FormData {
    name: string;
    email: string;
    age: number;
    phone: string;
  }

  const validateForm = (data: FormData): ValidationError[] => {
    const errors: ValidationError[] = [];

    // 验证1:姓名
    if (!data.name || data.name.trim().length === 0) {
      errors.push({
        field: "name",
        message: "姓名必填",
        value: data.name,
      });
    } else if (data.name.length < 2) {
      errors.push({
        field: "name",
        message: "姓名至少2个字符",
        value: data.name,
      });
    }

    // 验证2:邮箱
    if (!data.email) {
      errors.push({
        field: "email",
        message: "邮箱必填",
        value: data.email,
      });
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
      errors.push({
        field: "email",
        message: "邮箱格式无效",
        value: data.email,
      });
    }

    // 验证3:年龄
    if (data.age < 0 || data.age > 150) {
      errors.push({
        field: "age",
        message: "年龄必须在0到150之间",
        value: data.age,
      });
    }

    // 验证4:电话
    if (data.phone && !/^\d{3}-\d{3}-\d{4}$/.test(data.phone)) {
      errors.push({
        field: "phone",
        message: "电话格式必须为XXX-XXX-XXXX",
        value: data.phone,
      });
    }

    return errors;
  };

  // 示例1:表单有多个错误
  console.log(`[1] 表单验证有多个错误:
`);

  const invalidForm: FormData = {
    name: "",
    email: "not-an-email",
    age: 200,
    phone: "invalid",
  };

  const validationErrors = validateForm(invalidForm);

  yield* Effect.log(`[验证] 找到${validationErrors.length}个错误:
`);

  for (const error of validationErrors) {
    yield* Effect.log(`  ✗ ${error.field}: ${error.message}`);
  }

  // 示例2:批处理与部分成功
  console.log(`
[2] 批处理(积累成功和失败):
`);

  interface Record {
    id: string;
    data: string;
  }

  const processRecord = (record: Record): Result<string> => {
    if (record.id.length === 0) {
      return { success: false, error: "缺少ID" };
    }

    if (record.data.includes("ERROR")) {
      return { success: false, error: "无效数据" };
    }

    return { success: true, value: `processed-${record.id}` };
  };

  interface Result<T> {
    success: boolean;
    value?: T;
    error?: string;
  }

  const records: Record[] = [
    { id: "rec1", data: "ok" },
    { id: "", data: "ok" }, // 错误:缺少ID
    { id: "rec3", data: "ok" },
    { id: "rec4", data: "ERROR" }, // 错误:无效数据
    { id: "rec5", data: "ok" },
  ];

  const results: ProcessingResult<string> = {
    successes: [],
    errors: [],
  };

  for (const record of records) {
    const result = processRecord(record);

    if (result.success) {
      results.successes.push(result.value!);
    } else {
      results.errors.push({
        field: record.id || "unknown",
        message: result.error!,
      });
    }
  }

  yield* Effect.log(
    `[批处理] 处理了${records.length}条记录`
  );
  yield* Effect.log(`[批处理] ✓ ${results.successes.length} 成功`);
  yield* Effect.log(`[批处理] ✗ ${results.errors.length} 失败
`);

  for (const success of results.successes) {
    yield* Effect.log(`  ✓ ${success}`);
  }

  for (const error of results.errors) {
    yield* Effect.log(`  ✗ [${error.field}] ${error.message}`);
  }

  // 示例3:多步骤验证与错误积累
  console.log(`
[3] 多步骤验证(所有检查运行):
`);

  interface ServiceHealth {
    diskSpace: boolean;
    memory: boolean;
    network: boolean;
    database: boolean;
  }

  const diagnostics: ValidationError[] = [];

  // 检查1:磁盘空间
  const diskFree = 50; // MB

  if (diskFree < 100) {
    diagnostics.push({
      field: "disk-space",
      message: `只有${diskFree}MB可用(需要100MB)`,
      value: diskFree,
    });
  }

  // 检查2:内存
  const memUsage = 95; // 百分比

  if (memUsage > 85) {
    diagnostics.push({
      field: "memory",
      message: `使用${memUsage}%(阈值:85%)`,
      value: memUsage,
    });
  }

  // 检查3:网络
  const latency = 500; // 毫秒

  if (latency > 200) {
    diagnostics.push({
      field: "network",
      message: `延迟${latency}ms(阈值:200ms)`,
      value: latency,
    });
  }

  // 检查4:数据库
  const dbConnections = 95;
  const dbMax = 100;

  if (dbConnections > dbMax * 0.8) {
    diagnostics.push({
      field: "database",
      message: `${dbConnections}/${dbMax}个连接(80%阈值)`,
      value: dbConnections,
    });
  }

  if (diagnostics.length === 0) {
    yield* Effect.log(`[健康] ✓ 所有系统正常
`);
  } else {
    yield* Effect.log(
      `[健康] ✗ 检测到${diagnostics.length}个问题:
`
    );

    for (const diag of diagnostics) {
      yield* Effect.log(`  ⚠ ${diag.field}: ${diag.message}`);
    }
  }

  // 示例4:错误收集与重试决策
  console.log(`
[4] 错误收集用于重试策略:
`);

  interface ErrorWithContext {
    operation: string;
    error: string;
    retryable: boolean;
    timestamp: Date;
  }

  const operationErrors: ErrorWithContext[] = [];

  const operations = [
    { name: "fetch-config", fail: false },
    { name: "connect-db", fail: true },
    { name: "load-cache", fail: true },
    { name: "start-server", fail: false },
  ];

  for (const op of operations) {
    if (op.fail) {
      operationErrors.push({
        operation: op.name,
        error: "操作失败",
        retryable: op.name !== "fetch-config",
        timestamp: new Date(),
      });
    }
  }

  yield* Effect.log(`[操作] ${operationErrors.length}个错误:
`);

  for (const err of operationErrors) {
    const status = err.retryable ? "🔄 可重试" : "❌ 不可重试";
    yield* Effect.log(`  ${status}: ${err.operation}`);
  }

  if (operationErrors.every((e) => e.retryable)) {
    yield* Effect.log(`
[决策] 所有错误可重试,将重试
`);
  } else {
    yield* Effect.log(`
[决策] 有些不可重试错误,需要手动干预
`);
  }
});

Effect.runPromise(program);

原理:

错误积累策略:

  • 收集错误: 在报告前收集所有失败
  • 延迟失败: 尽管有错误,继续处理
  • 上下文错误: 保持错误位置/操作信息
  • 错误摘要: 汇总报告
  • 部分成功: 返回有效结果+错误

模式:使用Cause聚合、Result类型或自定义错误结构


快速失败导致问题:

问题1:表单验证

  • 用户提交有10个字段错误的表单
  • 第一个错误失败:“姓名必填”
  • 用户修复姓名,再次提交
  • 新错误:“邮箱无效”
  • 用户提交10次修复所有错误
  • 沮丧,生产力降低

问题2:批处理

  • 处理1000条记录,第5条记录失败
  • 995条记录未处理
  • 用户手动重试
  • 为每种错误类型重复
  • 低效

问题3:系统诊断

  • 服务健康检查失败
  • 报告:“检查1失败”
  • 修复检查1,服务仍宕机
  • 隐藏问题:检查2、3、4也失败
  • 浪费时间诊断

解决方案:

错误积累:

  • 运行所有验证
  • 收集错误
  • 报告所有问题
  • 用户一次性修复,非10次

部分成功:

  • 处理所有记录
  • 跟踪成功和失败
  • 返回:“950成功,50失败”
  • 无需重新处理

全面诊断:

  • 运行所有检查
  • 报告所有失败
  • 快速根本原因分析
  • 更快解决

🟠 高级模式

错误处理模式2:错误传播和链

规则: 使用错误传播在效果链中保留上下文,以便在正确的抽象级别进行调试和恢复。

好例子:

此示例演示了带有上下文的错误传播。

import { Effect, Data, Cause } from "effect";

// 领域特定错误与上下文
class DatabaseError extends Data.TaggedError("DatabaseError")<{
  query: string;
  parameters: unknown[];
  cause: Error;
}> {}

class NetworkError extends Data.TaggedError("NetworkError")<{
  endpoint: string;
  method: string;
  statusCode?: number;
  cause: Error;
}> {}

class ValidationError extends Data.TaggedError("ValidationError")<{
  field: string;
  value: unknown;
  reason: string;
}> {}

class BusinessLogicError extends Data.TaggedError("BusinessLogicError")<{
  operation: string;
  context: Record<string, unknown>;
  originalError: Error;
}> {}

const program = Effect.gen(function* () {
  console.log(`
[错误传播] 带有上下文的错误链
`);

  // 示例1:简单错误传播
  console.log(`[1] 通过层的错误传播:
`);

  const lowLevelOperation = Effect.gen(function* () {
    yield* Effect.log(`[层1] 低级操作开始`);

    yield* Effect.fail(new Error("文件未找到"));
  });

  const midLevelOperation = lowLevelOperation.pipe(
    Effect.mapError((error) =>
      new DatabaseError({
        query: "SELECT * FROM users",
        parameters: ["id=123"],
        cause: error instanceof Error ? error : new Error(String(error)),
      })
    )
  );

  const highLevelOperation = midLevelOperation.pipe(
    Effect.catchTag("DatabaseError", (dbError) =>
      Effect.gen(function* () {
        yield* Effect.log(`[层3] 捕获数据库错误`);
        yield* Effect.log(`[层3]   查询: ${dbError.query}`);
        yield* Effect.log(`[层3]   原因: ${dbError.cause.message}`);

        // 恢复决策
        return "fallback-value";
      })
    )
  );

  const result1 = yield* highLevelOperation;

  yield* Effect.log(`[结果] 恢复为: ${result1}
`);

  // 示例2:错误上下文积累
  console.log(`[2] 通过层积累上下文:
`);

  interface ErrorContext {
    timestamp: Date;
    operation: string;
    userId?: string;
    requestId: string;
  }

  const errorWithContext = (context: ErrorContext) =>
    Effect.fail(
      new BusinessLogicError({
        operation: context.operation,
        context: {
          userId: context.userId,
          timestamp: context.timestamp.toISOString(),
          requestId: context.requestId,
        },
        originalError: new Error("操作失败"),
      })
    );

  const myContext: ErrorContext = {
    timestamp: new Date(),
    operation: "process-payment",
    userId: "user-123",
    requestId: "req-abc-def",
  };

  const withContextRecovery = errorWithContext(myContext).pipe(
    Effect.mapError((error) => {
      // 记录完整上下文
      return {
        ...error,
        enriched: true,
        additionalInfo: {
          serviceName: "payment-service",
          environment: "production",
          version: "1.2.3",
        },
      };
    }),
    Effect.catchAll((error) =>
      Effect.gen(function* () {
        yield* Effect.log(`[错误捕获] ${error.operation}`);
        yield* Effect.log(`[上下文] ${JSON.stringify(error.context, null, 2)}`);
        return "recovered";
      })
    )
  );

  yield* withContextRecovery;

  // 示例3:网络错误与重试上下文
  console.log(`
[3] 网络错误与重试上下文:
`);

  interface RetryContext {
    attempt: number;
    maxAttempts: number;
    delay: number;
  }

  let attemptCount = 0;

  const networkCall = Effect.gen(function* () {
    attemptCount++;

    yield* Effect.log(`[尝试] ${attemptCount}/3`);

    if (attemptCount < 3) {
      yield* Effect.fail(
        new NetworkError({
          endpoint: "https://api.example.com/data",
          method: "GET",
          statusCode: 503,
          cause: new Error("服务不可用"),
        })
      );
    }

    return "success";
  });

  const withRetryContext = Effect.gen(function* () {
    let lastError: NetworkError | null = null;

    for (let i = 1; i <= 3; i++) {
      const result = yield* networkCall.pipe(
        Effect.catchTag("NetworkError", (error) => {
          lastError = error;

          yield* Effect.log(
            `[重试] 尝试${i}失败: ${error.statusCode}`
          );

          if (i < 3) {
            yield* Effect.log(`[重试] 重试前等待...`);
          }

          return Effect.fail(error);
        })
      ).pipe(
        Effect.tap(() => Effect.log(`[成功] 第${i}次尝试连接成功`))
      ).pipe(
        Effect.catchAll(() => Effect.succeed(null))
      );

      if (result !== null) {
        return result;
      }
    }

    if (lastError) {
      yield* Effect.fail(lastError);
    }

    return null;
  });

  const networkResult = yield* withRetryContext.pipe(
    Effect.catchAll((error) =>
      Effect.gen(function* () {
        yield* Effect.log(`[耗尽] 所有重试失败`);
        return "fallback";
      })
    )
  );

  yield* Effect.log(`
`);

  // 示例4:多层错误转换
  console.log(`[4] 层间错误转换:
`);

  const layer1Error = Effect.gen(function* () {
    yield* Effect.fail(new Error("原始系统错误"));
  });

  // 层2:转换为领域错误
  const layer2 = layer1Error.pipe(
    Effect.mapError((error) =>
      new DatabaseError({
        query: "SELECT ...",
        parameters: [],
        cause: error instanceof Error ? error : new Error(String(error)),
      })
    )
  );

  // 层3:转换为业务错误
  const layer3 = layer2.pipe(
    Effect.mapError((dbError) =>
      new BusinessLogicError({
        operation: "fetch-user-profile",
        context: {
          dbError: dbError.query,
        },
        originalError: dbError.cause,
      })
    )
  );

  // 层4:返回用户友好错误
  const userFacingError = layer3.pipe(
    Effect.mapError((bizError) => ({
      message: "无法加载个人资料",
      code: "PROFILE_LOAD_FAILED",
      originalError: bizError.originalError.message,
    })),
    Effect.catchAll((userError) =>
      Effect.gen(function* () {
        yield* Effect.log(`[用户消息] ${userError.message}`);
        yield* Effect.log(`[代码] ${userError.code}`);
        yield* Effect.log(`[调试] ${userError.originalError}`);
        return null;
      })
    )
  );

  yield* userFacingError;

  // 示例5:并发操作中的错误聚合
  console.log(`
[5] 并发操作中的错误传播:
`);

  const operation = (id: number, shouldFail: boolean) =>
    Effect.gen(function* () {
      if (shouldFail) {
        yield* Effect.fail(
          new Error(`操作${id}失败`)
        );
      }

      return `result-${id}`;
    });

  const concurrent = Effect.gen(function* () {
    const results = yield* Effect.all(
      [
        operation(1, false),
        operation(2, true),
        operation(3, false),
      ],
      { concurrency: 3 }
    ).pipe(
      Effect.catchAll((errors) =>
        Effect.gen(function* () {
          yield* Effect.log(`[并发] 捕获聚合错误`);

          // 实际代码中,Cause提供错误详情
          yield* Effect.log(`[错误] 并发执行期间遇到错误`);

          return [];
        })
      )
    );

    return results;
  });

  yield* concurrent;

  yield* Effect.log(`
[演示] 错误传播完成`);
});

Effect.runPromise(program);

原理:

错误传播保留上下文:

  • 原因链: 保持原始错误+上下文
  • 堆栈跟踪: 保留执行历史
  • 错误上下文: 添加操作名称、参数
  • 错误映射: 在层间转换错误
  • 恢复点: 决定在哪里处理错误

模式:使用mapError()tapError()catchAll()Cause.prettyPrint()


错误上下文丢失导致问题:

问题1:无用的错误消息

  • 用户看到:“错误: null”
  • 调试:从哪里来?何时?为什么?
  • 浪费数小时搜索日志

问题2:错误的恢复层

  • 网络错误 → 在业务逻辑层恢复(低效)
  • 应在网络层恢复 → 重试、指数退避

问题3:错误上下文丢失

  • 数据库连接失败
  • 但哪个数据库?哪个查询?什么参数?
  • 日志显示“连接失败”(不可操作)

问题4:隐藏根本原因

  • 效果1失败 → 触发效果2 → 不同错误
  • 开发者看到效果2错误
  • 不知道效果1是根本原因
  • 修复错误的东西

解决方案:

错误上下文:

  • 包括操作名称
  • 包括相关参数
  • 包括时间戳
  • 包括重试计数

错误原因链:

  • 保持原始错误
  • 在每个层添加上下文
  • mapError()来转换
  • tapError()来记录上下文

恢复层:

  • 低级:重试网络请求
  • 中级:转换领域错误
  • 高级:转换为用户友好消息

错误处理模式3:自定义错误策略

规则: 使用标记错误和自定义错误类型,以实现类型安全的错误处理和业务逻辑感知的恢复策略。

好例子:

此示例演示了自定义错误策略。

import { Effect, Data, Schedule } from "effect";

// 自定义领域错误
class NetworkError extends Data.TaggedError("NetworkError")<{
  endpoint: string;
  statusCode?: number;
  retryable: boolean;
}> {}

class ValidationError extends Data.TaggedError("ValidationError")<{
  field: string;
  reason: string;
}> {}

class AuthenticationError extends Data.TaggedError("AuthenticationError")<{
  reason: "invalid-token" | "expired-token" | "missing-token";
}> {}

class PermissionError extends Data.TaggedError("PermissionError")<{
  resource: string;
  action: string;
}> {}

class RateLimitError extends Data.TaggedError("RateLimitError")<{
  retryAfter: number; // 毫秒
}> {}

class NotFoundError extends Data.TaggedError("NotFoundError")<{
  resource: string;
  id: string;
}> {}

// 恢复策略选择器
const selectRecoveryStrategy = (
  error: Error
): "retry" | "fallback" | "fail" | "user-message" => {
  if (error instanceof NetworkError && error.retryable) {
    return "retry";
  }

  if (error instanceof RateLimitError) {
    return "retry"; // 带退避
  }

  if (error instanceof ValidationError) {
    return "user-message"; // 用户可修复
  }

  if (error instanceof NotFoundError) {
    return "fallback"; // 使用空结果
  }

  if (
    error instanceof AuthenticationError &&
    error.reason === "expired-token"
  ) {
    return "retry"; // 刷新令牌
  }

  if (error instanceof PermissionError) {
    return "fail"; // 不重试
  }

  return "fail"; // 默认:不重试
};

const program = Effect.gen(function* () {
  console.log(
    `
[自定义错误策略] 领域感知的错误处理
`
  );

  // 示例1:类型安全的错误处理
  console.log(`[1] 类型安全的错误捕获:
`);

  const operation1 = Effect.fail(
    new ValidationError({
      field: "email",
      reason: "无效格式",
    })
  );

  const handled1 = operation1.pipe(
    Effect.catchTag("ValidationError", (error) =>
      Effect.gen(function* () {
        yield* Effect.log(`[捕获] 验证错误`);
        yield* Effect.log(`  字段: ${error.field}`);
        yield* Effect.log(`  原因: ${error.reason}
`);

        return "validation-failed";
      })
    )
  );

  yield* handled1;

  // 示例2:多种错误类型与不同恢复
  console.log(`[2] 每种错误类型的不同恢复:
`);

  interface ApiResponse {
    status: number;
    body?: unknown;
  }

  const callApi = (shouldFail: "network" | "validation" | "ratelimit" | "success") =>
    Effect.gen(function* () {
      switch (shouldFail) {
        case "network":
          yield* Effect.fail(
            new NetworkError({
              endpoint: "https://api.example.com/data",
              statusCode: 503,
              retryable: true,
            })
          );

        case "validation":
          yield* Effect.fail(
            new ValidationError({
              field: "id",
              reason: "必须为数字",
            })
          );

        case "ratelimit":
          yield* Effect.fail(
            new RateLimitError({
              retryAfter: 5000,
            })
          );

        case "success":
          return { status: 200, body: { id: 123 } };
      }
    });

  // 测试每种错误类型
  const testCases = ["network", "validation", "ratelimit", "success"] as const;

  for (const testCase of testCases) {
    const strategy = yield* callApi(testCase).pipe(
      Effect.catchTag("NetworkError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[网络] 可重试: ${error.retryable}, 状态: ${error.statusCode}`
          );

          return "will-retry";
        })
      ),
      Effect.catchTag("ValidationError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[验证] ${error.field}: ${error.reason} (不重试)`
          );

          return "user-must-fix";
        })
      ),
      Effect.catchTag("RateLimitError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[速率限制] 重试后 ${error.retryAfter}ms`
          );

          return "retry-with-backoff";
        })
      ),
      Effect.catchAll((error) =>
        Effect.gen(function* () {
          yield* Effect.log(`[成功] 获取响应`);

          return "completed";
        })
      )
    );

    yield* Effect.log(`  策略: ${strategy}
`);
  }

  // 示例3:基于错误的自定义重试策略
  console.log(`[3] 错误特定的重试策略:
`);

  let attemptCount = 0;

  const networkOperation = Effect.gen(function* () {
    attemptCount++;

    yield* Effect.log(`[尝试] ${attemptCount}`);

    if (attemptCount === 1) {
      yield* Effect.fail(
        new NetworkError({
          endpoint: "api.example.com",
          statusCode: 502,
          retryable: true,
        })
      );
    }

    if (attemptCount === 2) {
      yield* Effect.fail(
        new RateLimitError({
          retryAfter: 100,
        })
      );
    }

    return "success";
  });

  // 类型安全的重试与错误分类
  let result3: string | null = null;

  for (let i = 0; i < 3; i++) {
    result3 = yield* networkOperation.pipe(
      Effect.catchTag("NetworkError", (error) =>
        Effect.gen(function* () {
          if (error.retryable && i < 2) {
            yield* Effect.log(`[重试] 网络错误可重试`);

            return null; // 信号重试
          }

          yield* Effect.log(`[失败] 网络错误不可重试`);

          return Effect.fail(error);
        })
      ),
      Effect.catchTag("RateLimitError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[退避] 速率限制,等待 ${error.retryAfter}ms`
          );

          yield* Effect.sleep(`${error.retryAfter} millis`);

          return null; // 信号重试
        })
      ),
      Effect.catchAll((error) =>
        Effect.gen(function* () {
          yield* Effect.log(`[错误] 未处理: ${error}`);

          return Effect.fail(error);
        })
      )
    ).pipe(
      Effect.catchAll(() => Effect.succeed(null))
    );

    if (result3 !== null) {
      break;
    }
  }

  yield* Effect.log(`
[结果] ${result3}
`);

  // 示例4:错误感知的业务逻辑
  console.log(`[4] 错误处理的业务逻辑:
`);

  interface User {
    id: string;
    email: string;
  }

  const loadUser = (id: string): Effect.Effect<User, NetworkError | NotFoundError> =>
    Effect.gen(function* () {
      if (id === "invalid") {
        yield* Effect.fail(
          new NotFoundError({
            resource: "user",
            id,
          })
        );
      }

      if (id === "network-error") {
        yield* Effect.fail(
          new NetworkError({
            endpoint: "/api/users",
            retryable: true,
          })
        );
      }

      return { id, email: `user-${id}@example.com` };
    });

  const processUser = (id: string) =>
    loadUser(id).pipe(
      Effect.catchTag("NotFoundError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[业务] 用户未找到: ${error.id}`
          );

          // 返回默认/空用户
          return { id: "", email: "" };
        })
      ),
      Effect.catchTag("NetworkError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[业务] 网络错误,将从缓存重试`
          );

          return { id, email: "cached@example.com" };
        })
      )
    );

  yield* processUser("valid-id");

  yield* processUser("invalid");

  yield* processUser("network-error");

  // 示例5:判别联合用于完整性检查
  console.log(`
[5] 完整性检查(编译时安全):
`);

  const classifyError = (
    error: NetworkError | ValidationError | AuthenticationError | PermissionError
  ): string => {
    switch (error._tag) {
      case "NetworkError":
        return `网络: ${error.statusCode}`;

      case "ValidationError":
        return `验证: ${error.field}`;

      case "AuthenticationError":
        return `认证: ${error.reason}`;

      case "PermissionError":
        return `权限: ${error.action}`;

      // TypeScript确保所有情况覆盖
      default:
        const _exhaustive: never = error;
        return _exhaustive;
    }
  };

  const testError = new ValidationError({
    field: "age",
    reason: "必须>=18",
  });

  const classification = classifyError(testError);

  yield* Effect.log(`[分类] ${classification}`);

  // 示例6:恢复策略链
  console.log(`
[6] 链式恢复策略:
`);

  const resilientOperation = Effect.gen(function* () {
    yield* Effect.fail(
      new RateLimitError({
        retryAfter: 50,
      })
    );
  });

  const withRecovery = resilientOperation.pipe(
    Effect.catchTag("RateLimitError", (error) =>
      Effect.gen(function* () {
        yield* Effect.log(
          `[步骤1] 捕获速率限制,等待 ${error.retryAfter}ms`
        );

        yield* Effect.sleep(`${error.retryAfter} millis`);

        // 再次尝试
        return yield* Effect.succeed("recovered");
      })
    ),
    Effect.catchTag("NetworkError", (error) =>
      Effect.gen(function* () {
        if (error.retryable) {
          yield* Effect.log(`[步骤2] 网络错误,重试中...`);

          return "retry";
        }

        return yield* Effect.fail(error);
      })
    ),
    Effect.catchAll((error) =>
      Effect.gen(function* () {
        yield* Effect.log(`[步骤3] 最终回退`);

        return "fallback";
      })
    )
  );

  yield* withRecovery;
});

Effect.runPromise(program);

原理:

自定义错误策略实现业务逻辑:

  • 标记错误: 使用Effect.Data实现类型安全的错误
  • 错误分类: 可重试、瞬态、永久
  • 领域语义: 业务有意义的错误
  • 恢复策略: 每种错误类型不同
  • 错误上下文: 包括恢复提示

模式:使用Data.TaggedError、错误判别器、catchTag()


通用错误阻止最佳恢复:

问题1:一刀切的重试

  • 网络超时(瞬态,带退避重试)
  • 无效API密钥(永久,不重试)
  • 两者相同处理 = 错误恢复

问题2:丢失业务意图

  • 系统错误:“连接被拒绝”
  • 业务含义:不明确
  • 用户消息:“出现问题”(无帮助)

问题3:错误的恢复层

  • 应在网络层重试
  • 反而在应用层重试
  • 浪费计算,用户体验差

问题4:静默失败

  • 多种错误类型可能
  • 通用捕获忽略区别
  • 错误:将错误A当作错误B处理
  • 数据损坏,难以调试

解决方案:

标记错误:

  • NetworkErrorValidationErrorPermissionError
  • 类型系统确保处理
  • TypeScript编译器捕获遗漏情况
  • 清晰意图

恢复策略:

  • NetworkError → 带指数退避重试
  • ValidationError → 返回用户消息,不重试
  • PermissionError → 记录安全事件,不重试
  • TemporaryError → 带抖动重试

业务语义:

  • 错误类型匹配领域概念
  • 代码像领域语言一样读
  • 更易维护
  • 新开发者快速理解