# Modeling Errors with Effect

Thrown errors work, but they hide one thing: when you call a function,
**what can fail**?

This tutorial shows how Effect makes each error part of a function's return type,
so TypeScript tracks it through every caller.

You use two functions throughout the tutorial:

| Function           | Description                                                                                                       |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `authorizePayment` | Simulates the payment provider: a sandbox token produces an authorization, a decline, or one of the errors below. |
| `processPayment`   | Calls `authorizePayment`. This second function shows what happens to errors when one function calls another.      |

`authorizePayment` can fail three ways:

- The card is expired.
- The token is not valid.
- The provider is unreachable.

Keep those three in mind. Each one becomes visible to TypeScript later.

## Thrown errors are invisible

A typical TypeScript function signature says what it needs and what it returns.
It says nothing about what the function can throw.

Here are common implementations of `authorizePayment` and `processPayment`
without Effect:

```ts
// Input: sandbox tokens make results reproducible
type PaymentToken =
  | "tok_authorized" // The provider authorizes the payment
  | "tok_declined" // The provider declines the payment
  | "tok_expiredcard" // The card is expired
  | "tok_invalid" // The token is not valid
  | "tok_timeout" // The provider is unreachable

// Output
interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

// Errors
class ExpiredCardError extends Error {}

class InvalidTokenError extends Error {}

class NetworkError extends Error {}

// Simulates the payment provider
const authorizePayment = async (
  token: PaymentToken,
): Promise<Authorization> => {
  if (token === "tok_expiredcard") {
    throw new ExpiredCardError()
  }

  if (token === "tok_invalid") {
    throw new InvalidTokenError()
  }

  if (token === "tok_timeout") {
    throw new NetworkError()
  }

  if (token === "tok_declined") {
    return { id: "pauth_456", status: "declined" }
  }

  if (token === "tok_authorized") {
    return { id: "pauth_123", status: "authorized" }
  }

  throw new InvalidTokenError()
}

// Calls the payment provider from application code
const processPayment = async (token: PaymentToken) => {
  try {
    const payment = await authorizePayment("tok_declined")
  } catch (error) {
    // `error` is `unknown`. TypeScript does not know which errors
    // `authorizePayment` can throw.
    if (error instanceof ExpiredCardError) {
      // ...
    }

    if (error instanceof InvalidTokenError) {
      // ...
    }

    if (error instanceof NetworkError) {
      // ...
    }

    throw error
  }
}
```

`authorizePayment` can throw the three errors listed above, but its return type
is only `Promise<Authorization>`. To discover those errors, a caller must read
the implementation or rely on documentation that may fall out of date.

{/* prettier-ignore */}
```ts
//                       ┌─ Needs `token`
//                       │                       ┌─ Returns `Authorization`
//                       ▼                       ▼
const authorizePayment: (token: PaymentToken) => Promise<Authorization>
//                                               └─ How can it fail?
```

Because the signature does not name the errors, TypeScript cannot check how
`processPayment` handles them. The missing type information causes four problems:

- **Blindness**: in `catch (error)`, TypeScript gives the caught value the type
  `unknown`, so it cannot tell which error `authorizePayment` threw.
- **Over-specification**: `processPayment` can handle an error that
  `authorizePayment` never throws, and TypeScript still compiles it.
- **Under-specification**: `processPayment` can omit a handler for an error that
  `authorizePayment` throws, and TypeScript still compiles it.
- **Drift**: if `authorizePayment` starts throwing a new error, the handlers in
  `processPayment` fall out of sync without causing a compile error.

## Make each error visible to TypeScript

A caller needs three pieces of information about a function:

1. What the function needs.
2. What it returns on success.
3. How it can fail.

The signature of `authorizePayment` provides only the first two.

Imagine the signature named those errors with a union, so callers could see all
three without reading the implementation:

{/* prettier-ignore */}
```ts
// Not real TypeScript
const authorizePayment: (token: PaymentToken) => Promise<Authorization> throws
  ExpiredCardError | InvalidTokenError | NetworkError
```

Unfortunately, TypeScript has no `throws` clause, so thrown errors cannot appear
in the function's type. One alternative is to treat an expected failure as a
value and return either it or the successful value:

```ts
// `ok` tells the caller whether the outcome contains an authorization or an error.
type AuthorizationOutcome =
  | { readonly ok: true; readonly authorization: Authorization }
  | {
      readonly ok: false
      readonly error: ExpiredCardError | InvalidTokenError | NetworkError
    }

// The Promise resolves to one of the two outcomes above.
const authorizePayment: (token: PaymentToken) => Promise<AuthorizationOutcome>
```

`AuthorizationOutcome` exposes both cases at the type level. At runtime, the
caller checks `ok`. This becomes repetitive: every caller must inspect the
result and return any error it cannot handle.

The Effect library removes this manual work by propagating unhandled failures.
Its `Effect<Success, Error>` type records both possible outcomes:

{/* prettier-ignore */}
```ts
//                       ┌─ What the function needs
//                       │                              ┌─ What it returns on success
//                       │                              │              ┌─ How it can fail
//                       ▼                              ▼              ▼
const authorizePayment: (token: PaymentToken) => Effect<Authorization, ExpiredCardError | InvalidTokenError | NetworkError>
```

For now, focus on the return type. `Authorization` is the success value. The
union contains the expected errors, which are failures a caller can handle.
Effect puts them in the `Error` type parameter, and TypeScript tracks them
through every caller.

### Keep error types distinct

The `Error` type parameter in `Effect<Success, Error>` can list several expected
errors as a union. This tells callers which failures they may need to handle. At
runtime, the program fails with one of them, and the caller may respond
differently to each error, such as retrying a network request or asking for a
different card.

There is a problem, however, with the original error definitions:

```ts
class ExpiredCardError extends Error {}

class InvalidTokenError extends Error {}

class NetworkError extends Error {}
```

These classes are distinct at runtime, so `instanceof` works. At the type level,
however, they have the same shape. TypeScript cannot distinguish them in an
`Effect`'s `Error` union.

The Effect library solves this with `Data.TaggedError`. Give each error an
identifier that is unique across your application:

```ts
import { Data } from "effect"

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}

class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}

class NetworkError extends Data.TaggedError("NetworkError") {}

const error = new ExpiredCardError()
console.log(error._tag)
// Output: "ExpiredCardError"
```

These identifiers keep the error types distinct in the union.
`Data.TaggedError` stores each identifier in a `_tag` property, which Effect
later uses to select the matching error handler.

> **Why use a class?**
>
> In TypeScript a class is both a type and a value. One declaration gives you
> the error type to name in a signature and the constructor to build it.

### Rewrite the payment flow with Effect

Now you build the program with Effect. An `Effect` is lazily executed: creating one doesn't run it. It describes a program that can succeed or fail.

Here is the same two-step program, first with a `Promise`, then with an `Effect`:

<div class="not-prose my-6 grid grid-cols-1 gap-x-6 md:grid-cols-2">

<div>

**Promise**

```ts
interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

const authorizePayment = async (
  token: PaymentToken,
): Promise<Authorization> => {
  if (token === "tok_declined") {
    return { id: "pauth_456", status: "declined" }
  }

  return { id: "pauth_123", status: "authorized" }
}
// ---cut---
const program = async () => {
  const first = await authorizePayment("tok_authorized")
  const second = await authorizePayment("tok_declined")

  return { first: first.status, second: second.status }
}

await program() // => { first: "authorized", second: "declined" }
```

</div>

<div>

**Effect**

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

const authorizePayment = (token: PaymentToken): Effect.Effect<Authorization> =>
  Effect.succeed(
    token === "tok_declined"
      ? { id: "pauth_456", status: "declined" }
      : { id: "pauth_123", status: "authorized" },
  )
// ---cut---
const program = Effect.gen(function* () {
  const first = yield* authorizePayment("tok_authorized")
  const second = yield* authorizePayment("tok_declined")

  return { first: first.status, second: second.status }
})

await Effect.runPromise(program) // => { first: "authorized", second: "declined" }
```

</div>

</div>

Same shape, three swaps:

- `async function` becomes `Effect.gen(function* ...)`
- `await` becomes `yield*`
- `await program()` becomes `await Effect.runPromise(program)`

If a step returns an error, the rest do not run, the same way a thrown error stops an `async`
function.

Now refactor `authorizePayment` with Effect.

`return yield*` returns the same way as a success, and the rest of the `Effect` stops.
There is no `throw` and no outer `catch`. The error gets added the `Error` type parameter,
where TypeScript tracks it:

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}

class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}

class NetworkError extends Data.TaggedError("NetworkError") {}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"
// ---cut---
const authorizePayment = (
  token: PaymentToken,
): Effect.Effect<
  Authorization,
  ExpiredCardError | InvalidTokenError | NetworkError
> =>
  Effect.gen(function* () {
    if (token === "tok_expiredcard") {
      return yield* new ExpiredCardError()
    }

    if (token === "tok_invalid") {
      return yield* new InvalidTokenError()
    }

    if (token === "tok_timeout") {
      return yield* new NetworkError()
    }

    if (token === "tok_declined") {
      return { id: "pauth_456", status: "declined" }
    }

    return { id: "pauth_123", status: "authorized" }
  })
```

Refactor `processPayment` with Effect. Now calls `authorizePayment` and returns its result. It
inherits the three errors, so it sees them:

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}

class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}

class NetworkError extends Data.TaggedError("NetworkError") {}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

const authorizePayment = (
  token: PaymentToken,
): Effect.Effect<
  Authorization,
  ExpiredCardError | InvalidTokenError | NetworkError
> =>
  Effect.gen(function* () {
    if (token === "tok_expiredcard") {
      return yield* new ExpiredCardError()
    }

    if (token === "tok_invalid") {
      return yield* new InvalidTokenError()
    }

    if (token === "tok_timeout") {
      return yield* new NetworkError()
    }

    if (token === "tok_declined") {
      return { id: "pauth_456", status: "declined" }
    }

    return { id: "pauth_123", status: "authorized" }
  })
// ---cut---
//    ┌─ (token: PaymentToken) => Effect.Effect<Authorization, ExpiredCardError | InvalidTokenError | NetworkError>
//    ▼
const processPayment = (token: PaymentToken) =>
  Effect.gen(function* () {
    const authorization = yield* authorizePayment(token)

    return authorization
  })

await Effect.runPromise(processPayment("tok_authorized")) // => { id: "pauth_123", status: "authorized" }
```

TypeScript inferred the `Error` type parameter as the union of the three errors
you return.

> **More delegation to TypeScript**
>
> TypeScript infers both type parameters from the steps you build, so you rarely
> write an `Effect` signature by hand.

> **Checkpoint**
>
> You now build the work with `Effect.gen`, take each success with `yield*`, and
> fail by returning a tagged error with `return yield*`.

## Handle the expected errors you modeled

The signature of `authorizePayment` lists the expected errors. The caller uses the list to handle them.

`Effect.catchTag` takes the `_tag` of one error and a handler. The handler is an `Effect` that receives the typed error as a parameter, not `unknown`.

The example uses method `.pipe` (available to every `Effect`), to apply the handler to the `Effect`, like method chaining:

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}
class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}
class NetworkError extends Data.TaggedError("NetworkError") {}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

const processPayment = (
  token: PaymentToken,
): Effect.Effect<
  Authorization,
  ExpiredCardError | InvalidTokenError | NetworkError
> =>
  Effect.gen(function* () {
    if (token === "tok_expiredcard") {
      return yield* new ExpiredCardError()
    }

    if (token === "tok_invalid") {
      return yield* new InvalidTokenError()
    }

    if (token === "tok_timeout") {
      return yield* new NetworkError()
    }

    return { id: "pauth_123", status: "authorized" }
  })
// ---cut---
const outcome = processPayment("tok_expiredcard").pipe(
  Effect.catchTag("ExpiredCardError", (error) =>
    //                                 └─ `ExpiredCardError`
    Effect.succeed("Card expired, ask for another"),
  ),
)

await Effect.runPromise(outcome) // => "Card expired, ask for another"
```

The handled error leaves the `Error` type parameter. The others stay:

{/* prettier-ignore */}
```ts
//                                    ┌─ `ExpiredCardError` is removed
//                                    ▼
Effect.Effect<string | Authorization, InvalidTokenError | NetworkError>
```

> **Success type widens too**
>
> The handler returns `Effect.succeed("Card expired, ask for another")`, a
>   `string`, while the original success was an `Authorization`.
>
> So the `Success` type parameter now covers both: `string | Authorization`.

TypeScript now has another job: it won't compile a handler for a `_tag` that isn't in the union.

`Effect.catchTags` (notice the `s` at the end) handles several errors at once with one table: an object keyed by `_tag`.

For each `_tag`, its handler receives the typed error:

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}
class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}
class NetworkError extends Data.TaggedError("NetworkError") {}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

const processPayment = (
  token: PaymentToken,
): Effect.Effect<
  Authorization,
  ExpiredCardError | InvalidTokenError | NetworkError
> =>
  Effect.gen(function* () {
    if (token === "tok_expiredcard") {
      return yield* new ExpiredCardError()
    }

    if (token === "tok_invalid") {
      return yield* new InvalidTokenError()
    }

    if (token === "tok_timeout") {
      return yield* new NetworkError()
    }

    return { id: "pauth_123", status: "authorized" }
  })
// ---cut---
const outcome = processPayment("tok_timeout").pipe(
  Effect.catchTags({
    //                 ┌─ `ExpiredCardError`
    //                 ▼
    ExpiredCardError: (error) =>
      Effect.succeed("Card expired, ask for another"),

    //             ┌─ `NetworkError`
    //             ▼
    NetworkError: (error) => Effect.succeed("Provider unreachable, try again"),
  }),
)

await Effect.runPromise(outcome) // => "Provider unreachable, try again"
```

Two more rules TypeScript enforces:

1. Every key must be a `_tag` from the union. A typo does not compile.
2. The result keeps what the table misses: `Effect.Effect<Authorization, InvalidTokenError>`.

When all the errors are handled, the `Error` type parameter becomes `never`:

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}
class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}
class NetworkError extends Data.TaggedError("NetworkError") {}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

declare const processPayment: (
  token: PaymentToken,
) => Effect.Effect<
  Authorization,
  ExpiredCardError | InvalidTokenError | NetworkError
>
// ---cut---
//    ┌─ (token: PaymentToken) => Effect.Effect<string | Authorization, never>
//    ▼
const handle = (token: PaymentToken) =>
  processPayment(token).pipe(
    Effect.catchTags({
      ExpiredCardError: () => Effect.succeed("..."),
      InvalidTokenError: () => Effect.succeed("..."),
      NetworkError: () => Effect.succeed("..."),
    }),
  )
```

> **Why `never`?**
>
> `never` is TypeScript's empty type: the union with no members left. Each
> handled tag drops out of the union, and once the last one is gone there is
> nothing to name, so the type is `never`. It is the type-level way of saying
> "this can no longer fail."

## Modeling unexpected errors: defects

Every error so far is one a caller can handle. These are expected errors. But `never` does not mean the code cannot fail.

One condition must always hold: an authorization is captured once, never twice. This condition is an invariant. If it breaks, the payment step charges the customer twice.

When an invariant breaks, no handler can respond. The safe action is to stop and report.

Effect calls this failure an **unexpected error**, or a **defect**: an impossible state, a broken invariant, or a bug in third-party code.

A defect is not tracked in the `Error` type parameter. The runtime records it in a `Cause`.

The capture step enforces this invariant. The ledger fixture starts with the payment already captured. So this run is the retry, and the invariant breaks.

No caller can undo a double capture. A typed error would promise a handler that cannot exist. The step dies instead. `Effect.die` sends the error to the `Cause`, and the `Error` type parameter stays empty.

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}

class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}

class NetworkError extends Data.TaggedError("NetworkError") {}

class DoubleCaptureError extends Data.TaggedError("DoubleCaptureError")<{
  readonly id: string
}> {}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

const processPayment = (
  token: PaymentToken,
): Effect.Effect<
  Authorization,
  ExpiredCardError | InvalidTokenError | NetworkError
> =>
  Effect.gen(function* () {
    if (token === "tok_expiredcard") {
      return yield* new ExpiredCardError()
    }

    if (token === "tok_invalid") {
      return yield* new InvalidTokenError()
    }

    if (token === "tok_timeout") {
      return yield* new NetworkError()
    }

    return { id: "pauth_123", status: "authorized" }
  })

// Tutorial fixture: an in-memory ledger, so captures are reproducible.
// The first attempt already captured the payment. This run is the retry.
const captured = new Set(["pauth_123"])

//    ┌─ Effect.Effect<Authorization>: no `Error` type parameter
//    ▼
const capturePayment = (
  authorization: Authorization,
): Effect.Effect<Authorization> =>
  Effect.gen(function* () {
    if (captured.has(authorization.id)) {
      // Invariant broken: the step dies instead of failing with a
      // typed error no caller could handle.
      return yield* Effect.die(
        new DoubleCaptureError({
          id: authorization.id,
        }),
      )
    }

    captured.add(authorization.id)

    return authorization
  })

const program = Effect.gen(function* () {
  const payment = yield* processPayment("tok_authorized")

  return yield* capturePayment(payment).pipe(
    Effect.catchDefect((defect) =>
      defect instanceof DoubleCaptureError
        ? Effect.succeed(`terminated: ${defect.id} already captured`)
        : Effect.die(defect),
    ),
  )
})

await Effect.runPromise(program) // => "terminated: pauth_123 already captured"
```

The signature promises `Effect.Effect<Authorization>` with no `Error` type parameter. Yet the double capture fails the run. The failure never entered the type. It became a defect, and `Effect.catchDefect` caught it. Without that handler, `Effect.runPromise` rejects. Unknown defects stay fatal, so the handler re-raises them with `Effect.die`.

### Remove errors no caller can handle from the type

`Effect.orDie` is a modeling decision. No caller can handle these errors, so stop tracking them. Every typed error becomes a defect, and the `Error` type parameter disappears.

Apply it at the boundary between your Effect code and its callers. Inside, the code still fails with typed errors. The signature the caller sees has none left.

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}
class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}
class NetworkError extends Data.TaggedError("NetworkError") {}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

const processPayment = (
  token: PaymentToken,
): Effect.Effect<
  Authorization,
  ExpiredCardError | InvalidTokenError | NetworkError
> =>
  Effect.gen(function* () {
    if (token === "tok_expiredcard") {
      return yield* new ExpiredCardError()
    }

    if (token === "tok_invalid") {
      return yield* new InvalidTokenError()
    }

    if (token === "tok_timeout") {
      return yield* new NetworkError()
    }

    return { id: "pauth_123", status: "authorized" }
  })
// ---cut---
//    ┌─ Effect.Effect<Authorization>: `Error` type parameter is gone
//    ▼
const reconcile = processPayment("tok_expiredcard").pipe(Effect.orDie)

try {
  await Effect.runPromise(reconcile)
} catch (defect) {
  defect instanceof ExpiredCardError && defect._tag // => "ExpiredCardError"
}
```

A nightly reconciliation job is one such boundary. When the card is expired, the job crashes and alerts its owner. The failure crossed the boundary as a defect. So `Effect.runPromise` rejected with the `ExpiredCardError` itself.

### Handle defects at the application boundary

A defect is a bug. A handler cannot fix it. But the boundary decides what happens next, once, in one place: the job runner, the HTTP handler, the batch loop. For each defect, choose:

1. Report it, and let the program stop.
2. Or catch it, report it, and let the program continue.

If a caller can respond to the failure, it is not a defect. Model it as a typed error instead.

Inside the program, defects stay fatal. Only the boundary decides.

`Effect.catchDefect` handles defects only. The next example shows that typed errors pass through unchanged.

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

interface Authorization {
  readonly id: string
  readonly status: "authorized" | "declined"
}

class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}
class InvalidTokenError extends Data.TaggedError("InvalidTokenError") {}
class NetworkError extends Data.TaggedError("NetworkError") {}
class DoubleCaptureError extends Data.TaggedError("DoubleCaptureError")<{
  readonly id: string
}> {}

type PaymentToken =
  | "tok_authorized"
  | "tok_declined"
  | "tok_expiredcard"
  | "tok_invalid"
  | "tok_timeout"

const processPayment = (
  token: PaymentToken,
): Effect.Effect<
  Authorization,
  ExpiredCardError | InvalidTokenError | NetworkError
> =>
  Effect.gen(function* () {
    if (token === "tok_expiredcard") {
      return yield* new ExpiredCardError()
    }

    if (token === "tok_invalid") {
      return yield* new InvalidTokenError()
    }

    if (token === "tok_timeout") {
      return yield* new NetworkError()
    }

    return { id: "pauth_123", status: "authorized" }
  })
// ---cut---
const outcome = processPayment("tok_expiredcard").pipe(
  Effect.catchDefect((defect) =>
    defect instanceof DoubleCaptureError
      ? Effect.succeed(`reported: ${defect.id} already captured`)
      : Effect.die(defect),
  ),
)

try {
  await Effect.runPromise(outcome)
} catch (failure) {
  failure instanceof ExpiredCardError && failure._tag // => "ExpiredCardError"
}
```

The `catchDefect` handler never ran. The result is `catch (failure)`, not `catch (defect)`. The typed error was never a defect. It reached `runPromise` unchanged, exactly as its type said, with all three members still in the `Error` type parameter.

A rule of thumb for the whole section:

- Prefer the typed operators for the errors you modeled: `Effect.catchTag`
  and `Effect.catchTags`.
- Use `Effect.catchDefect` only where handling the unexpected is
  intentional and safe.

> **Checkpoint**
>
> "Did I cover every case?" has two answers now. For the errors you modeled, the
> `Error` type parameter answers. It shrinks as you handle them. For the errors
> you did not model, the boundary answers. `catchDefect` sees them at the edge,
> where you report them and let the program stop.

## Let's review what we built

You can now do five things. Each one maps to an operator you used.

- Read an `Effect` signature as a contract. The `Success` type parameter names the result. The `Error` type parameter names every error a caller can handle.
- Make each error visible to TypeScript with `Data.TaggedError`. TypeScript tracks it instead of losing it as `unknown` in a `catch`.
- Return errors instead of throwing them. Inside `Effect.gen`, fail with `return yield* new XError()`. The `Error` type parameter tracks each one.
- Handle one `_tag` with `Effect.catchTag`, and many with `Effect.catchTags`. Each handled tag leaves the `Error` union. An empty union is `never`.
- Keep defects apart from typed errors. `Effect.die` records a defect in the `Cause`. `Effect.catchDefect` handles defects at the boundary. `Effect.orDie` converts tracked errors you no longer handle.

## Further reading

Each guide below builds on one part of this tutorial.

- [Expected Errors](/docs/v4/error-management/expected-errors/), to answer an expired card or a timeout with a fallback value.
- [Unexpected Errors](/docs/v4/error-management/unexpected-errors/), to report a double capture or an impossible status at the boundary.
- [Yieldable Errors](/docs/v4/error-management/yieldable-errors/), to add a new tagged error to the signature.
