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
- Lost Error Types: Promise's catch only gives you
unknowntype. 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 - 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
- Difficult Error Recovery: Network request failures need retries, database disconnections need failover, but after 3 levels of try-catch nesting the code is unreadable
- 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
- 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
-
Schema Validation Integration: Use
@effect/schemato automatically validate input data at Effect pipeline entry points. Validation failures auto-generate type-safe ValidationErrors, reducing manual validation code by 80% -
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
-
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
-
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
-
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.
Recommended Online Tools
- 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 →