# 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.

```ts
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:

1. **Recovering** — the `catch*` family, which removes errors from `E`.
2. **Transforming** — `mapError`, `mapBoth`, `orDie`, `sandbox`, which reshape
   the error channel without (necessarily) succeeding.
3. **Observing & swallowing** — `tap*Error`/`tapCause` to run side effects on
   failure, and `ignore`/`eventually` to discard failures entirely.

## 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.

```ts
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

When you want a *different* recovery for each error, `Effect.catchTags` takes an
object keyed by tag. Each handler again receives its narrowed error.

```ts
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

`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.
**Caution:** In Effect v4 the catch-all is named `Effect.catch`, not `catchAll`. It only
  recovers from typed failures — defects and interruptions pass through
  untouched. Use [`catchCause`](#catchcause--recover-from-defects-too) to reach
  those.

```ts
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

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.

```ts
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

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`](https://effect.plants.sh/error-management/result-and-exit/).

```ts
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

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.

```ts
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

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

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.

```ts
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

Recovers from the whole `Cause` only when a predicate over the cause returns
`true`. Pairs with the `Cause.has*` predicates (`hasFails`, `hasDies`,
`hasInterrupts`).

```ts
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

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.

```ts
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

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.

```ts
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

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.

```ts
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

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.

```ts
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](https://effect.plants.sh/error-management/reason-errors/) page, which owns the pattern.

## 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

Transforms the failure value while leaving the success value untouched. The
effect still fails with the new error type; it does not recover.

```ts
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

Transforms both channels at once, without changing whether the effect succeeds
or fails. Takes `onFailure` and `onSuccess`.

```ts
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
})
```

### orDie

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.

```ts
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

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.

```ts
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

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

Runs an effect on a typed failure, then keeps failing with the original error.
Ideal for logging or metrics.

```ts
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

Taps only when the failure's `_tag` matches (one tag or an array of tags). The
handler receives the narrowed error.

```ts
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 NetworkError
```

### tapCause

Taps the entire `Cause`, so it observes typed failures, defects, *and*
interruptions. Use it for catch-all failure logging.

```ts
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 fails
```

### tapCauseIf

Taps the `Cause` only when a predicate over it returns `true`.

```ts
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 fails
```

### tapCauseFilter

Taps the `Cause` only when a `Filter` selects it; the
side effect receives the selected value and the original cause.

```ts
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 defect
```

### tapDefect

Taps only defects (thrown exceptions / `Effect.die`). Recoverable typed failures
are ignored.

```ts
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

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

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.

```ts
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

Like `ignore`, but also swallows the full `Cause`, including defects and
interruptions — not just typed failures. Supports the same `log` / `message`
options.

```ts
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

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`.

```ts
import { Console, Effect } from "effect"

let attempts = 0
const 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"
```
**Note:** `eventually` retries forever with no backoff. For bounded retries with delays
  and conditions, use [`Effect.retry`](https://effect.plants.sh/error-management/fallback-and-retry/).

## Next steps

- Handle a fixed set of sub-causes inside one error with
  [Reason Errors](https://effect.plants.sh/error-management/reason-errors/).
- Retry instead of recover with
  [Fallback & Retry](https://effect.plants.sh/error-management/fallback-and-retry/).
- Fold success *and* failure together with [Matching](https://effect.plants.sh/error-management/matching/).
- Understand the `Cause` structure that `catchCause`/`sandbox` expose in
  [Result & Exit](https://effect.plants.sh/error-management/result-and-exit/).