Error Management in Effect
Overview
Effect distinguishes between two types of failures:
- Expected Errors (Recoverable) - Represented in the
Errortype parameter, tracked at compile time - Defects (Unexpected/Unrecoverable) - Runtime exceptions, bugs, not in type signature
Effect<Success, Error, Requirements>;
// ^^^^^ Expected errors live here
Creating Typed Errors
Using Schema.TaggedError (Recommended)
import { Schema, Effect } from "effect";
class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", { userId: Schema.String }) {}
// Note: Schema.Unknown is semantically correct here because `cause` captures
// arbitrary caught exceptions whose type is genuinely unknown at the domain level.
// This is NOT type weakening - JavaScript exceptions can be any value.
class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", { cause: Schema.Unknown }) {}
const getUser = (id: string): Effect.Effect<User, UserNotFound | NetworkError> =>
Effect.gen(function* () {
// ...implementation
return yield* Effect.fail(new UserNotFound({ userId: id }));
});
Using Effect.fail
const divide = (a: number, b: number) => (b === 0 ? Effect.fail(new DivisionByZero()) : Effect.succeed(a / b));
Catching and Recovering from Errors
catchAll - Catch All Errors
program.pipe(Effect.catchAll((error) => Effect.succeed("fallback value")));
catchTag - Catch Specific Error by Tag
const program = getUser(id).pipe(
Effect.catchTag("UserNotFound", (error) => Effect.succeed(defaultUser)),
Effect.catchTag("NetworkError", (error) => Effect.retry(Schedule.exponential("1 second"))),
);
catchTags - Handle Multiple Error Types
const program = getUser(id).pipe(
Effect.catchTags({
UserNotFound: (error) => Effect.succeed(defaultUser),
NetworkError: (error) => Effect.fail(new ServiceUnavailable()),
}),
);
orElse - Provide Fallback Effect
const primary = fetchFromPrimary();
const fallback = fetchFromBackup();
const resilient = primary.pipe(Effect.orElse(() => fallback));
orElseSucceed - Provide Fallback Value
const program = fetchConfig().pipe(Effect.orElseSucceed(() => defaultConfig));
Transforming Errors
mapError - Transform Error Type
const program = rawApiCall().pipe(Effect.mapError((error) => new ApiError({ cause: error })));
mapBoth - Transform Both Success and Error
const program = effect.pipe(
Effect.mapBoth({
onError: (e) => new WrappedError({ cause: e }),
onSuccess: (a) => a.toUpperCase(),
}),
);
Error Accumulation
When running multiple effects, collect all errors instead of failing fast:
Using Effect.all with mode: "either"
const results = yield * Effect.all([effect1, effect2, effect3], { mode: "either" });
Using Effect.partition
const [failures, successes] = yield * Effect.partition(items, (item) => processItem(item));
Using Effect.validate
const result = yield * Effect.validate([check1, check2, check3], { concurrency: "unbounded" });
Defects (Unexpected Errors)
Defects are bugs/unexpected failures not tracked in types:
const defect = Effect.die(new Error("Unexpected!"));
const program = effect.pipe(Effect.orDie);
const sandboxed = Effect.sandbox(program);
Cause - Full Error Information
The Cause type contains complete failure information:
import { Cause, Match } from "effect";
// In sandbox, you get full Cause - use Match for handling
const handled = Effect.sandbox(program).pipe(
Effect.catchAll((cause) =>
Match.value(cause).pipe(
Match.when(Cause.isFailure, () => {
// Expected error
return Effect.succeed(fallback);
}),
Match.when(Cause.isDie, () => {
// Defect - log and recover
return Effect.succeed(fallback);
}),
Match.when(Cause.isInterrupt, () => {
// Interruption
return Effect.succeed(fallback);
}),
Match.orElse(() => Effect.succeed(fallback)),
),
),
);
Retrying
import { Schedule } from "effect";
const resilient = effect.pipe(
Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.jittered, Schedule.compose(Schedule.recurs(5)))),
);
// Retry with condition - use Match.tag for error type checking
const conditional = effect.pipe(
Effect.retry({
schedule: Schedule.recurs(3),
while: (error) =>
Match.value(error).pipe(
Match.tag("NetworkError", () => true),
Match.orElse(() => false),
),
}),
);
Timeouts
const withTimeout = effect.pipe(Effect.timeout("5 seconds"));
const failOnTimeout = effect.pipe(
Effect.timeoutFail({
duration: "5 seconds",
onTimeout: () => new TimeoutError(),
}),
);
Error Matching Patterns
Using Effect.match
const result =
yield *
effect.pipe(
Effect.match({
onFailure: (error) => `Failed: ${error.message}`,
onSuccess: (value) => `Success: ${value}`,
}),
);
Using Effect.matchEffect
const result =
yield *
effect.pipe(
Effect.matchEffect({
onFailure: (error) => logError(error).pipe(Effect.as("failed")),
onSuccess: (value) => logSuccess(value).pipe(Effect.as("success")),
}),
);
Best Practices
- Use TaggedError for all domain errors - Enables
catchTagpattern matching - Keep error channel for recoverable errors - Use defects for bugs
- Transform errors at boundaries - Map low-level errors to domain errors
- Use typed errors generously - The compiler tracks them for free
- Accumulate validation errors - Don't fail fast when validating
- Only use Schema.Unknown for genuinely untyped values - The
causefield on error types is the canonical example (caught JS exceptions can be any value). Never use Schema.Unknown or Schema.Any for fields whose shape you can describe - define proper schemas instead.
Additional Resources
For comprehensive error management documentation, consult ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt.
Search for these sections:
- "Expected Errors" for creating typed errors
- "Error Accumulation" for collecting multiple errors
- "Sandboxing" for handling defects
- "Retrying" for retry policies
- "Timing Out" for timeout patterns
- "Two Types of Errors" for error philosophy