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大痛点

  1. 错误类型丢失:Promise 的 catch 只能拿到 unknown 类型,运行时才知道错误是什么,编译器无法帮你检查未处理的错误,线上经常出现"undefined is not a function"
  2. 错误处理不一致:团队中有人用 try-catch,有人用 .catch(),有人用 Result 模式,有人直接 throw,错误处理风格混乱,新人完全不知道该怎么写
  3. 错误恢复困难:网络请求失败需要重试,数据库连接断开需要切换备库,但 try-catch 嵌套3层后代码已经不可读了
  4. 错误组合爆炸:函数A可能抛出错误E1,函数B可能抛出错误E2,组合后错误类型是 E1|E2,再组合就变成 E1|E2|E3|E4...类型推导完全失控
  5. 错误日志缺失上下文: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依赖关系,消除循环

进阶优化

  1. Schema验证集成:使用@effect/schema在Effect管道入口处自动验证输入数据,验证失败自动生成类型安全的ValidationError,减少80%的手动验证代码

  2. 分布式追踪集成:通过自定义Layer集成OpenTelemetry,每个Effect自动携带traceId和spanId,实现端到端的分布式追踪,排障时间从小时级降至分钟级

  3. 错误聚合与去重:开发错误聚合中间件,相同错误在时间窗口内只报告一次,减少90%的告警噪音,同时保留错误频率统计

  4. 自适应重试策略:基于历史错误率和响应时间的自适应重试算法,自动调整退避参数和重试次数,在错误率飙升时主动降低重试频率保护下游

  5. 错误预算管理:实现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应用的完整方法论。

在线工具推荐

  • JSON格式化 - 格式化Effect错误报告和日志输出
  • cURL转代码 - 将API调试cURL转为Effect-TS服务代码
  • 哈希计算 - 计算错误报告校验哈希,确保日志完整性

本站提供浏览器本地工具,免注册即可试用 →

#Effect-TS#TypeScript错误处理#函数式编程#2026#前端工程