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);

// 方式2:安全運行,獲取Exit
const result2 = await Effect.runPromiseExit(program);

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";

// ====== 基礎重試策略 ======

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))),
        Schedule.compose(Schedule.recurs(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)),
          )
        )
      )
    )
  );

// ====== 降級策略 ======

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";

// ====== 錯誤組合 ======

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;
  });

// ====== 錯誤隔離 ======

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");

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}`));
    }),
});

const AppLayer = Layer.mergeAll(
  AuditServiceLive,
  NotificationServiceLive,
);

模式五:生產級錯誤管理與日誌集成

在生產環境中,錯誤管理需要與日誌、監控、告警系統深度集成。

// 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(
    withErrorLogging,
    withErrorMetrics,
    Effect.catchAll((error: E) =>
      Effect.gen(function* (_) {
        const report = createErrorReport(error);

        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);
        }),
  );

// ====== 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;
  }
}

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 };
}

踩坑指南

坑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#前端工程