# Reason Errors

Sometimes a single operation can fail in several related ways, and you want them
grouped under *one* error type rather than scattered across the error channel. A
**reason error** is a tagged error with a `reason` field whose value is itself a
union of tagged errors. The outer error names the operation; the inner reason
names exactly what went wrong.

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

// Each reason is an ordinary tagged error...
class RateLimitError extends Schema.TaggedErrorClass<RateLimitError>()(
  "RateLimitError",
  { retryAfter: Schema.Number }
) {}

class QuotaExceededError extends Schema.TaggedErrorClass<QuotaExceededError>()(
  "QuotaExceededError",
  { limit: Schema.Number }
) {}

class SafetyBlockedError extends Schema.TaggedErrorClass<SafetyBlockedError>()(
  "SafetyBlockedError",
  { category: Schema.String }
) {}

// ...and the outer error carries one of them in a `reason` field.
class AiError extends Schema.TaggedErrorClass<AiError>()("AiError", {
  reason: Schema.Union([RateLimitError, QuotaExceededError, SafetyBlockedError])
}) {}

declare const callModel: Effect.Effect<string, AiError>
```

The error channel stays clean — callers see a single `AiError` — while the
`reason` preserves the full detail for anyone who wants to drill in. This is the
shape used throughout Effect's own [AI](https://effect.plants.sh/ai/) modules.

## catchReason — handle one reason

`Effect.catchReason` takes the outer error tag, the reason tag to handle, a
handler for that reason, and an optional catch-all for the remaining reasons.

```ts
import { Effect } from "effect"
// AiError and its reason errors (RateLimitError, QuotaExceededError,
// SafetyBlockedError) are the ones declared in the first example above.

declare const callModel: Effect.Effect<string, AiError>

const handleOneReason = callModel.pipe(
  Effect.catchReason(
    "AiError", // the outer error _tag
    "RateLimitError", // the reason _tag to catch
    // handler for the caught reason — note `reason.retryAfter` is available
    (reason) => Effect.succeed(`Retry after ${reason.retryAfter} seconds`),
    // optional catch-all for the other reasons
    (reason) => Effect.succeed(`Model call failed for reason: ${reason._tag}`)
  )
)
```

## catchReasons — a handler per reason

`Effect.catchReasons` handles several reasons of one error at once, keyed by
reason tag — the reason-level analogue of
[`catchTags`](https://effect.plants.sh/error-management/catching-errors/).

```ts
import { Effect } from "effect"
// AiError and its reason errors carry over from the first example above.

declare const callModel: Effect.Effect<string, AiError>

const handleMultipleReasons = callModel.pipe(
  Effect.catchReasons("AiError", {
    RateLimitError: (reason) =>
      Effect.succeed(`Retry after ${reason.retryAfter} seconds`),
    QuotaExceededError: (reason) =>
      Effect.succeed(`Quota exceeded at ${reason.limit} tokens`)
    // Any reasons you omit remain in the AiError that flows through.
  })
)
```

As with `catchReason`, you can pass a final catch-all handler as the third
argument to cover every remaining reason.

## unwrapReason — promote reasons to the error channel

If you would rather treat each reason as a first-class error, `Effect.unwrapReason`
*flattens* the reasons out of the wrapper and into the error channel. After
unwrapping you can use the ordinary [`catchTags`](https://effect.plants.sh/error-management/catching-errors/)
machinery.

```ts
import { Effect } from "effect"
// AiError and its reason errors carry over from the first example above.

declare const callModel: Effect.Effect<string, AiError>

const unwrapAndHandle = callModel.pipe(
  // AiError disappears; RateLimitError | QuotaExceededError | SafetyBlockedError
  // now flow directly in the error channel.
  Effect.unwrapReason("AiError"),
  Effect.catchTags({
    RateLimitError: (reason) =>
      Effect.succeed(`Back off for ${reason.retryAfter} seconds`),
    QuotaExceededError: (reason) =>
      Effect.succeed(`Increase quota beyond ${reason.limit}`),
    SafetyBlockedError: (reason) =>
      Effect.succeed(`Blocked by safety category: ${reason.category}`)
  })
)
```
**Tip:** Reach for reason errors when a set of failures is *closed and operation-local*
  — e.g. all the ways one API call can be rejected. If the failures are
  independent and may originate from different places, plain
  [tagged errors](https://effect.plants.sh/error-management/tagged-errors/) in the error channel are
  simpler.

## Next steps

- Build resilience around these errors with
  [Fallback & Retry](https://effect.plants.sh/error-management/fallback-and-retry/).
- See reason errors in practice in the [AI](https://effect.plants.sh/ai/) modules.