Catching Errors
Once an error is in the E channel, you recover from it with one of the catch
combinators. Each handler returns a new effect, and — crucially — handling an
error removes it from the error type, so the compiler always knows which
failures are still outstanding.
import { Effect, Schema } from "effect"
class ParseError extends Schema.TaggedErrorClass<ParseError>()("ParseError", { input: Schema.String}) {}
class ReservedPortError extends Schema.TaggedErrorClass<ReservedPortError>()( "ReservedPortError", { port: Schema.Number }) {}
declare const loadPort: ( input: string) => Effect.Effect<number, ParseError | ReservedPortError>
// Catch BOTH tagged errors with a single `catchTag` call, returning a default.// ┌─── Effect<number, never> — every error handled// ▼const recovered = loadPort("80").pipe( Effect.catchTag(["ParseError", "ReservedPortError"], () => Effect.succeed(3000)))This page covers the whole error-side toolbox in three layers:
- Recovering — the
catch*family, which removes errors fromE. - Transforming —
mapError,mapBoth,orDie,sandbox, which reshape the error channel without (necessarily) succeeding. - Observing & swallowing —
tap*Error/tapCauseto run side effects on failure, andignore/eventuallyto discard failures entirely.
catchTag — recover from one tag
Section titled “catchTag — recover from one tag”Effect.catchTag matches a tagged error by its _tag. The handler receives the
narrowed error, so its fields are available, and the matched tag disappears from
the resulting error channel.
import { Effect, Schema } from "effect"
class ValidationError extends Schema.TaggedErrorClass<ValidationError>()( "ValidationError", { message: Schema.String }) {}
class NetworkError extends Schema.TaggedErrorClass<NetworkError>()( "NetworkError", { statusCode: Schema.Number }) {}
declare const fetchUser: ( id: string) => Effect.Effect<string, ValidationError | NetworkError>
const program = fetchUser("123").pipe( // Handle only ValidationError; NetworkError still flows through. Effect.catchTag("ValidationError", (error) => Effect.succeed(`invalid: ${error.message}`) ))// program: Effect<string, NetworkError>Pass an array of tags to handle several with the same handler, as in the opening example.
catchTags — a handler per tag
Section titled “catchTags — a handler per tag”When you want a different recovery for each error, Effect.catchTags takes an
object keyed by tag. Each handler again receives its narrowed error.
import { Effect, Schema } from "effect"
class ValidationError extends Schema.TaggedErrorClass<ValidationError>()( "ValidationError", { message: Schema.String }) {}
class NetworkError extends Schema.TaggedErrorClass<NetworkError>()( "NetworkError", { statusCode: Schema.Number }) {}
declare const fetchUser: ( id: string) => Effect.Effect<string, ValidationError | NetworkError>
const userOrFallback = fetchUser("123").pipe( Effect.catchTags({ ValidationError: (error) => Effect.succeed(`Validation failed: ${error.message}`), NetworkError: (error) => Effect.succeed(`Network request failed with status ${error.statusCode}`) }))// userOrFallback: Effect<string, never>catch — recover from any failure
Section titled “catch — recover from any failure”Effect.catch is the catch-all for typed failures. The handler receives the
whole E union, and the error channel collapses to whatever the handler can
still fail with.
import { Effect, Schema } from "effect"
class ParseError extends Schema.TaggedErrorClass<ParseError>()("ParseError", { input: Schema.String}) {}
class ReservedPortError extends Schema.TaggedErrorClass<ReservedPortError>()( "ReservedPortError", { port: Schema.Number }) {}
declare const loadPort: ( input: string) => Effect.Effect<number, ParseError | ReservedPortError>
const withFinalFallback = loadPort("invalid").pipe( // Handle one specific error first... Effect.catchTag("ReservedPortError", () => Effect.succeed(3000)), // ...then mop up anything still failing with a catch-all. Effect.catch(() => Effect.succeed(3000)))catchIf — recover when a predicate matches
Section titled “catchIf — recover when a predicate matches”When the discriminator isn’t a _tag, Effect.catchIf recovers based on a
predicate or refinement. With a refinement the handler receives the narrowed
error type; non-matching errors re-fail with the original cause.
import { Effect } from "effect"
declare const request: Effect.Effect<string, { status: number }>
const program = request.pipe( Effect.catchIf( (error) => error.status === 404, () => Effect.succeed("not found, using default") ))catchCause — recover from defects too
Section titled “catchCause — recover from defects too”Everything above ignores defects and interruptions by design. When you genuinely
need the full picture — to recover a plugin that threw, or to log a defect —
reach for Effect.catchCause. The handler receives the entire
Cause.
import { Cause, Effect } from "effect"
// `Effect.die` records a defect, so `catch`/`catchTag` would NOT see it.const program = Effect.die(new Error("boom"))
const recovered = program.pipe( Effect.catchCause((cause) => // Render the cause (failures + defects + interruptions) for logging. Effect.succeed(`recovered from: ${Cause.pretty(cause)}`) ))Catching and re-failing
Section titled “Catching and re-failing”A handler is just an effect, so it can transform an error instead of fully
recovering. Return a new Effect.fail to map one error to another, or
Effect.die to promote it to a defect.
import { Effect, Schema } from "effect"
class LowLevelError extends Schema.TaggedErrorClass<LowLevelError>()( "LowLevelError", { detail: Schema.String }) {}
class DomainError extends Schema.TaggedErrorClass<DomainError>()("DomainError", { message: Schema.String}) {}
declare const lowLevel: Effect.Effect<number, LowLevelError>
const mapped = lowLevel.pipe( // Translate an infrastructure error into a domain error. Effect.catchTag("LowLevelError", (error) => Effect.fail(new DomainError({ message: `wrapped: ${error.detail}` })) ))// mapped: Effect<number, DomainError>The full catch family
Section titled “The full catch family”The combinators above cover the common cases. Below is the complete recovery
reference — every catch* variant in Effect, with a short runnable example.
They split along two axes: what they match on (a tag, a predicate, a Filter,
the whole Cause, a specific built-in error) and whether non-matching failures
re-fail or pass through.
catchFilter
Section titled “catchFilter”Recovers from typed failures selected by a Filter. The
filter can narrow or transform the error before the handler runs; the optional
orElse handles filtered-out values, otherwise they re-fail.
import { Data, Effect, Filter } from "effect"
class NotFound extends Data.TaggedError("NotFound")<{ id: string }> {}
const program = Effect.fail(new NotFound({ id: "user-1" }))
const recovered = program.pipe( Effect.catchFilter( Filter.tagged("NotFound"), (error) => Effect.succeed(`missing:${error.id}`) ))// => Effect.runPromise(recovered) resolves to "missing:user-1"catchCauseIf
Section titled “catchCauseIf”Recovers from the whole Cause only when a predicate over the cause returns
true. Pairs with the Cause.has* predicates (hasFails, hasDies,
hasInterrupts).
import { Cause, Effect } from "effect"
const httpRequest = Effect.fail("Network Error")
const program = Effect.catchCauseIf( httpRequest, Cause.hasFails, // only catch causes that contain a typed failure (cause) => Effect.succeed(`Fallback after: ${Cause.squash(cause)}`))// => Effect.runPromise(program) resolves to "Fallback after: Network Error"catchCauseFilter
Section titled “catchCauseFilter”Like catchCauseIf, but the Filter selects (and can
transform) the cause. The handler receives both the filtered value and the
original Cause; filtered-out causes re-fail with the residual cause.
import { Cause, Effect, Filter } from "effect"
const task = Effect.die(new Error("boom"))
const program = task.pipe( Effect.catchCauseFilter( // keep only causes that contain a defect Filter.fromPredicate(Cause.hasDies), (_value, cause) => Effect.succeed(`contained a defect: ${Cause.squash(cause)}`) ))// => Effect.runPromise(program) resolves to "contained a defect: Error: boom"catchDefect
Section titled “catchDefect”Recovers from defects (thrown exceptions or values passed to Effect.die)
while leaving typed failures and interruptions in place. Use sparingly, at
integration boundaries where a thrown bug must be contained.
import { Effect } from "effect"
// A thrown error becomes a defect, invisible to `catch`/`catchTag`.const program = Effect.sync(() => { throw new Error("plugin crashed")})
const recovered = program.pipe( Effect.catchDefect((defect) => Effect.succeed(`contained: ${defect}`)))// => Effect.runPromise(recovered) resolves to "contained: Error: plugin crashed"catchNoSuchElement
Section titled “catchNoSuchElement”Catches Cause.NoSuchElementError specifically — the error produced by
Effect.fromNullishOr, Effect.fromOption, and Option-yielding gens — and
converts the result into an Option. Success becomes Option.some, the missing
error becomes Option.none, all other errors are preserved.
import { Effect } from "effect"
const some = Effect.fromNullishOr(1).pipe(Effect.catchNoSuchElement)const none = Effect.fromNullishOr(null).pipe(Effect.catchNoSuchElement)
Effect.runPromise(some).then(console.log) // => { _id: 'Option', _tag: 'Some', value: 1 }Effect.runPromise(none).then(console.log) // => { _id: 'Option', _tag: 'None' }catchReason / catchReasons / unwrapReason
Section titled “catchReason / catchReasons / unwrapReason”When a tagged error nests a reason error inside one of its fields, these
combinators let you recover from (or promote) the inner reason without unpacking
the wrapper by hand. unwrapReason replaces the parent error in E with its
inner reason union.
import { Data, Effect } from "effect"
class RateLimitError extends Data.TaggedError("RateLimitError")<{ retryAfter: number}> {}
class AiError extends Data.TaggedError("AiError")<{ reason: RateLimitError}> {}
declare const program: Effect.Effect<string, AiError>
// Before: Effect<string, AiError>// After: Effect<string, RateLimitError>const unwrapped = program.pipe(Effect.unwrapReason("AiError"))These three are covered in depth on the Reason Errors page, which owns the pattern.
Transforming errors, not recovering
Section titled “Transforming errors, not recovering”Sometimes you do not want to recover — you want to reshape the error channel:
translate one error type to another, collapse a typed failure into an
unrecoverable defect, or expose the whole Cause so you can pattern-match it.
mapError
Section titled “mapError”Transforms the failure value while leaving the success value untouched. The effect still fails with the new error type; it does not recover.
import { Data, Effect } from "effect"
class TaskError extends Data.TaggedError("TaskError")<{ message: string }> {}
// ┌─── Effect<number, string>// ▼const task = Effect.fail("Oh no!").pipe(Effect.as(1))
// ┌─── Effect<number, TaskError>// ▼const mapped = Effect.mapError(task, (message) => new TaskError({ message }))mapBoth
Section titled “mapBoth”Transforms both channels at once, without changing whether the effect succeeds
or fails. Takes onFailure and onSuccess.
import { Data, Effect } from "effect"
class TaskError extends Data.TaggedError("TaskError")<{ message: string }> {}
// ┌─── Effect<number, string>// ▼const task = Effect.fail("Oh no!").pipe(Effect.as(1))
// ┌─── Effect<boolean, TaskError>// ▼const modified = Effect.mapBoth(task, { onFailure: (message) => new TaskError({ message }), onSuccess: (n) => n > 0})Turns any typed failure into an unrecoverable defect, emptying the E channel
to never. Use it when a typed failure actually represents a bug that no caller
should handle.
import { Data, Effect } from "effect"
class DivideByZeroError extends Data.TaggedError("DivideByZeroError")<{}> {}
const divide = (a: number, b: number) => b === 0 ? Effect.fail(new DivideByZeroError()) : Effect.succeed(a / b)
// ┌─── Effect<number, never> — error channel emptied// ▼const program = Effect.orDie(divide(1, 0))
Effect.runPromise(program).catch(console.error)// => (FiberFailure) DivideByZeroError ...sandbox
Section titled “sandbox”Moves the entire Cause into the error channel, so E becomes
Cause<E>. Now a single catch/catchTag can pattern-match failures, defects,
and interruptions uniformly. Recover (or re-fail) and the cause structure
collapses back as usual.
import { Cause, Effect } from "effect"
const task = Effect.die(new Error("boom"))
// ┌─── Effect<never, Cause<never>>// ▼const sandboxed = Effect.sandbox(task)
const recovered = sandboxed.pipe( // The error is now a full Cause we can inspect. Effect.catch((cause) => Cause.hasDies(cause) ? Effect.succeed(`contained defect: ${Cause.squash(cause)}`) : Effect.failCause(cause) ))// => Effect.runPromise(recovered) resolves to "contained defect: Error: boom"Observing errors without recovering
Section titled “Observing errors without recovering”These are the failure-side mirror of the tap operators in
pipelines: they run a side effect when the effect fails, then re-propagate the
original failure unchanged. If the side effect itself fails, that error is added
to the channel.
tapError
Section titled “tapError”Runs an effect on a typed failure, then keeps failing with the original error. Ideal for logging or metrics.
import { Console, Effect } from "effect"
const task: Effect.Effect<number, string> = Effect.fail("NetworkError")
const tapping = Effect.tapError(task, (error) => Console.log(`expected error: ${error}`))// => logs "expected error: NetworkError", then still fails with "NetworkError"tapErrorTag
Section titled “tapErrorTag”Taps only when the failure’s _tag matches (one tag or an array of tags). The
handler receives the narrowed error.
import { Console, Data, Effect } from "effect"
class NetworkError extends Data.TaggedError("NetworkError")<{ statusCode: number}> {}
const task: Effect.Effect<number, NetworkError> = Effect.fail( new NetworkError({ statusCode: 504 }))
const program = Effect.tapErrorTag(task, "NetworkError", (error) => Console.log(`status: ${error.statusCode}`))// => logs "status: 504", then still fails with the NetworkErrortapCause
Section titled “tapCause”Taps the entire Cause, so it observes typed failures, defects, and
interruptions. Use it for catch-all failure logging.
import { Cause, Console, Effect } from "effect"
const task = Effect.fail("Something went wrong")
const program = Effect.tapCause(task, (cause) => Console.log(`Logging cause: ${Cause.squash(cause)}`))// => logs "Logging cause: Something went wrong", then still failstapCauseIf
Section titled “tapCauseIf”Taps the Cause only when a predicate over it returns true.
import { Cause, Console, Effect } from "effect"
const task = Effect.fail("Network timeout")
const program = Effect.tapCauseIf( task, Cause.hasFails, // skip interrupts and defects (cause) => Console.log(`failure cause: ${Cause.squash(cause)}`))// => logs "failure cause: Network timeout", then still failstapCauseFilter
Section titled “tapCauseFilter”Taps the Cause only when a Filter selects it; the
side effect receives the selected value and the original cause.
import { Cause, Console, Effect, Filter } from "effect"
const task = Effect.die(new Error("boom"))
const program = task.pipe( Effect.tapCauseFilter( Filter.fromPredicate(Cause.hasDies), (_value, cause) => Console.log(`defect: ${Cause.squash(cause)}`) ))// => logs "defect: Error: boom", then still fails with the defecttapDefect
Section titled “tapDefect”Taps only defects (thrown exceptions / Effect.die). Recoverable typed failures
are ignored.
import { Console, Effect } from "effect"
const recoverable: Effect.Effect<number, string> = Effect.fail("NetworkError")const fatal: Effect.Effect<number> = Effect.die("Something went wrong")
Effect.runFork(Effect.tapDefect(recoverable, (d) => Console.log(`defect: ${d}`)))// => no output (NetworkError is not a defect)
Effect.runFork(Effect.tapDefect(fatal, (d) => Console.log(`defect: ${d}`)))// => logs "defect: RuntimeException: Something went wrong"Swallowing errors
Section titled “Swallowing errors”When you want a failing effect to not propagate its failure at all — run it for its side effects, or just keep trying until it works.
ignore
Section titled “ignore”Discards both the success and the failure, producing Effect<void, never, R>.
Pass { log: true } (and optionally message) to emit the Cause to the logger
when the effect fails.
import { Effect } from "effect"
// ┌─── Effect<number, string>// ▼const task = Effect.fail("Uh oh!").pipe(Effect.as(5))
// ┌─── Effect<void, never>// ▼const program = task.pipe(Effect.ignore)
// Variant that still logs the failure as a warning before discarding it:const logged = task.pipe(Effect.ignore({ log: "Warn", message: "ignoring failure" }))ignoreCause
Section titled “ignoreCause”Like ignore, but also swallows the full Cause, including defects and
interruptions — not just typed failures. Supports the same log / message
options.
import { Effect } from "effect"
const task = Effect.die("boom") // a defect
// ┌─── Effect<void, never>// ▼const program = task.pipe(Effect.ignoreCause)
const logged = task.pipe(Effect.ignoreCause({ log: true, message: "ignoring cause" }))eventually
Section titled “eventually”Retries the effect until it succeeds, discarding every failure along the way, and
yielding between attempts so other fibers can run. The result has E = never.
import { Console, Effect } from "effect"
let attempts = 0const flaky = Effect.gen(function* () { attempts++ yield* Console.log(`Attempt ${attempts}`) if (attempts < 3) return yield* Effect.fail("Not ready") return "Ready"})
const program = Effect.eventually(flaky)// => runs until success: "Attempt 1", "Attempt 2", "Attempt 3", resolves to "Ready"Next steps
Section titled “Next steps”- Handle a fixed set of sub-causes inside one error with Reason Errors.
- Retry instead of recover with Fallback & Retry.
- Fold success and failure together with Matching.
- Understand the
Causestructure thatcatchCause/sandboxexpose in Result & Exit.