TypeScript Effect System Error Handling in Practice: 5 Core Patterns for Functional Error Management

前端工程

In 2026, the functional programming paradigm in the TypeScript ecosystem is reshaping how we handle errors. Effect-TS, as TypeScript's most powerful Effect system, makes "impossible to forget error handling" a reality through type-safe error composition, tagged errors, and declarative error recovery. Compared to traditional try-catch and Promise.catch, the Effect system encodes error types in function signatures — the compiler forces you to handle every possible error. This article takes a practical approach, diving deep into 5 core patterns of TypeScript Effect system error handling to help you build robust production-grade applications.

Core Concepts

Concept Description Use Case
Effect Lazy value describing a computation, may contain errors All side-effect operations
Effect.Success Successful Effect Pure computation
Effect.Failure Failed Effect Error propagation
Tagged Error Error type with a tag field Error classification & matching
Layer Dependency injection layer Service dependency management
Schedule Retry strategy scheduler Error recovery
Cause Error cause tracing Error diagnosis

Problem Analysis: 5 Pain Points

  1. Lost Error Types: Promise's catch only gives you unknown type. You don't know the error until runtime. The compiler can't check unhandled errors, leading to frequent "undefined is not a function" in production
  2. Inconsistent Error Handling: Some team members use try-catch, others use .catch(), some use the Result pattern, some just throw. Error handling styles are chaotic — new developers have no idea what to do
  3. Difficult Error Recovery: Network request failures need retries, database disconnections need failover, but after 3 levels of try-catch nesting the code is unreadable
  4. Error Composition Explosion: Function A might throw E1, Function B might throw E2, combined they're E1|E2, then E1|E2|E3|E4... type inference spirals out of control
  5. Missing Error Context in Logs: After catching an error, only error.message is logged — missing call chain, parameters, timing context. During production debugging, "error messages are incomprehensible, stack traces don't find the root cause"

Pattern 1: Effect-TS Basics & Type-Safe Errors

Effect's core philosophy: errors are part of the type system, not runtime surprises.

// src/effects/basic.ts - Effect basics
import { Effect } from "effect";

// ====== Traditional vs Effect approach ======

// ❌ Traditional: error type is lost
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}`);
    // Callers don't know this function might throw!
  }
  return response.json();
}

// ✅ Effect: errors encoded in types
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;
  });

// The type signature clearly tells you: this Effect may fail with FetchError | ParseError
// The compiler will force you to handle these errors!

// ====== Basic Effect operations ======

// Success value
const success: Effect.Effect<number, never, never> = Effect.succeed(42);

// Failure value
const failure: Effect.Effect<never, string, never> = Effect.fail("something went wrong");

// From sync computation
const fromSync = Effect.sync(() => {
  const value = Math.random();
  return value > 0.5 ? value : -1;
});

// From Promise
const fromPromise = Effect.tryPromise({
  try: () => fetch("https://api.toolsku.com/health"),
  catch: (error) => new FetchError(0, "https://api.toolsku.com/health"),
});

// ====== Running Effects ======

import { Runtime } from "effect";

const program = fetchUser("user-123");

// Method 1: Run with default Runtime
const result1 = await Effect.runPromise(program);

// Method 2: Safe run, get Exit
const result2 = await Effect.runPromiseExit(program);
// Exit<User, FetchError | ParseError>

if (result2._tag === "Success") {
  console.log("User:", result2.value);
} else {
  console.log("Error:", result2.cause);
}

// Method 3: Custom Runtime (recommended for production)
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 - Complete user service Effect
import { Effect, Context, Layer } from "effect";

// ====== Define error types ======

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

// ====== Define service interface ======

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

// ====== Implement business logic ======

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

Pattern 2: Tagged Errors & Error Classification

Tagged errors are the soul of the Effect system. Through the _tag field, you can precisely match and handle different error types at the type level.

// src/effects/tagged-errors.ts - Tagged errors and classification
import { Effect, Match } from "effect";

// ====== Define domain error hierarchy ======

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

// ====== Type-safe error handling with 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,  // Compiler guarantees all error types are handled!
  );
};

// ====== Error classification utilities ======

const isRetryable = (error: AppError): boolean => {
  switch (error._tag) {
    case "DatabaseConnectionError":
    case "CacheError":
      return true;  // Infrastructure errors are retryable
    case "UnauthorizedError":
    case "ForbiddenError":
    case "InsufficientBalanceError":
    case "DuplicateResourceError":
      return false;  // Business errors should not be retried
  }
};

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

Pattern 3: Error Recovery & Retry Strategies

Effect's Schedule system provides declarative retry strategies, making error recovery code both powerful and readable.

// src/effects/recovery.ts - Error recovery and retry
import { Effect, Schedule, Duration } from "effect";

// ====== Basic retry strategies ======

// Simple retry 3 times
const withSimpleRetry = <A, E, R>(
  effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
  effect.pipe(Effect.retry(Schedule.recurs(3)));

// Exponential backoff retry
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)),
      )
    )
  );

// Smart retry based on error type
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)),
          )
        )
      )
    )
  );

// ====== Fallback strategies ======

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

// Cache-based 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;
  });

// ====== Timeout & Racing ======

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

Pattern 4: Error Composition & Layer Dependencies

When multiple Effects are composed, error types automatically merge. The Layer system manages service dependencies.

// src/effects/composition.ts - Error composition and Layers
import { Effect, Context, Layer } from "effect";

// ====== Error composition ======

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

// ====== Error isolation ======

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 dependency management ======

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

Pattern 5: Production-Grade Error Management & Logging Integration

In production, error management needs deep integration with logging, monitoring, and alerting systems.

// src/effects/production.ts - Production-grade error management
import { Effect, Fiber, Metric, Schedule, Duration } from "effect";

// ====== Error logging middleware ======

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

// ====== Error metrics collection ======

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

// ====== Error reporting ======

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

// ====== Global error handler ======

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

// ====== Error boundary ======

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 handler integration ======

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

Pitfall Guide

Pitfall 1: Forgetting to Run Effects

// ❌ Wrong: Effects are lazy — nothing happens without running them
const fetchUser = (id: string) =>
  Effect.gen(function* (_) {
    const response = yield* _(Effect.tryPromise(() => fetch(`/api/users/${id}`)));
    return response;
  });

fetchUser("123");  // Nothing happens!
// ✅ Correct: Explicitly run the Effect
const program = fetchUser("123");
const result = await Effect.runPromise(program);

Pitfall 2: Using throw Inside Effects

// ❌ Wrong: Using throw bypasses the error type system
const parseJson = (input: string): Effect.Effect<unknown, never, never> =>
  Effect.sync(() => JSON.parse(input));  // throw becomes a Defect, not in the type!
// ✅ Correct: Use Effect.try or 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),
  });

Pitfall 3: Error Type Mismatch in catchAll

// ❌ Wrong: catchAll must handle all error types
const program = Effect.gen(function* (_) {
  const user = yield* _(fetchUser("123"));  // Error: FetchError | ParseError
}).pipe(
  Effect.catchAll((error: FetchError) =>  // ❌ ParseError not handled!
    Effect.succeed(null)
  )
);
// ✅ Correct: Use Match to handle all error types
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,
    )
  )
);

Pitfall 4: Missing Layer Causes Runtime Error

// ❌ Wrong: Using UserRepository without providing a Layer
const program = getUser("123");
await Effect.runPromise(program);  // Runtime error: Missing service: UserRepository
// ✅ Correct: Provide Layer before running
const program = getUser("123").pipe(
  Effect.provide(UserRepositoryLive),
);
await Effect.runPromise(program);

Pitfall 5: Forgetting yield* in gen Functions

// ❌ Wrong: Forgetting yield* — Effect won't execute
const program = Effect.gen(function* (_) {
  const user = fetchUser("123");  // ❌ Missing yield* _
  return user;  // Returns Effect, not User!
});
// ✅ Correct: Use yield* _() to unwrap Effects
const program = Effect.gen(function* (_) {
  const user = yield* _(fetchUser("123"));  // ✅ Correctly unwrapped
  return user;
});

Error Troubleshooting

Error Message Cause Solution
Missing service: X Layer not provided Use Effect.provide() to provide the Layer
Defect: Uncaught error throw used inside Effect Use Effect.try() or Effect.fail() instead
Timeout after X ms Effect execution timeout Check for infinite loops, increase timeout or add retry
Type error: Property '_tag' is missing Error type missing _tag field Add readonly _tag property to error class
Non-exhaustive match Match doesn't cover all cases Add Match.exhaustive to ensure completeness
Fiber interrupted Fiber was cancelled Check for timeout or manual interruption
Maximum call stack exceeded Infinite recursion Check recursive Effect has termination condition
Cannot read property of undefined Effect not run Ensure Effect.runPromise() is called
Schedule limit exceeded Retry count exhausted Check retry strategy, increase retries or fix root cause
Layer cycle detected Circular Layer dependencies Refactor Layer dependencies to eliminate cycles

Advanced Optimization

  1. Schema Validation Integration: Use @effect/schema to automatically validate input data at Effect pipeline entry points. Validation failures auto-generate type-safe ValidationErrors, reducing manual validation code by 80%

  2. Distributed Tracing Integration: Integrate OpenTelemetry through custom Layers. Each Effect automatically carries traceId and spanId, enabling end-to-end distributed tracing. Troubleshooting time drops from hours to minutes

  3. Error Aggregation & Deduplication: Develop error aggregation middleware that reports identical errors only once within a time window, reducing alert noise by 90% while preserving error frequency statistics

  4. Adaptive Retry Strategies: Adaptive retry algorithm based on historical error rates and response times. Automatically adjusts backoff parameters and retry counts. Proactively reduces retry frequency when error rates spike to protect downstream services

  5. Error Budget Management: Implement SLO error budget system. When error budget is exhausted, automatically trigger traffic switching or degradation to protect core business availability

Comparison

Dimension Effect-TS try-catch neverthrow ts-results
Type Safety ✅ Fully encoded ❌ Runtime ✅ Result type ✅ Result type
Error Composition ✅ Auto-merge ❌ Manual ⚠️ Manual ⚠️ Manual
Retry Strategies ✅ Schedule ❌ Manual ❌ None ❌ None
Dependency Injection ✅ Layer ❌ None ❌ None ❌ None
Concurrency Control ✅ Fiber ❌ None ❌ None ❌ None
Learning Curve Steep Gentle Moderate Moderate
Bundle Size ~50KB 0 ~5KB ~3KB
Ecosystem Integration Rich Native Limited Limited
Best For Large apps Small scripts Medium projects Medium projects

💡 Recommendation: If your project exceeds 100K lines and needs a complete error management system, Effect-TS is the best choice. For simplicity in smaller projects, neverthrow is sufficient. For simple scripts, try-catch works fine.

Summary

Errors aren't exceptions — they're part of computation. The Effect system embeds this philosophy into every corner of the type system. Error types are encoded in function signatures, the compiler forces you to handle every possible failure, and declarative retry and fallback make recovery logic both powerful and readable. Master these 5 core patterns — Effect basics & type-safe errors, tagged errors & classification, error recovery & retry, error composition & Layer dependencies, and production-grade error management — and you'll possess the complete methodology for building "impossible-to-break" TypeScript applications.

  • JSON Formatter - Format Effect error reports and log output
  • cURL to Code - Convert API debugging cURL to Effect-TS service code
  • Hash Calculator - Calculate error report verification hashes to ensure log integrity

Try these browser-local tools — no sign-up required →

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