---
title: Typed errors
description: .catch — render an exhaustive, compile-checked fallback for each error a REC can fail with.
group: Core
order: 4
---

A REC's failures are part of its type. Effect's error channel `E` rides along with every `yield*`, so
effract knows exactly how a component can fail. `.catch` turns that knowledge into UI: an **exhaustive,
compile-checked** map of fallbacks — one per error `_tag`, each handed exactly that error.

## `.catch`

```tsx
import { rec, query } from '@tmonier/effract';

const Profile = rec(function* () {
  const user = yield* query(fetchUser(id), id); // E = NotFound | Unauthorized
  return <Card user={user} />;
}).catch({
  NotFound: () => <Empty />,
  Unauthorized: (e) => <Login reason={e.reason} />, // e is the Unauthorized error, fully typed
});
```

The handler map is exhaustive over the error channel and checked by the compiler:

- **Forget a tag** the body can fail with → it won't compile (a missing property).
- **Invent a tag** the body can't raise → it won't compile (an excess property).
- Each handler receives **exactly its own error**, so `e.reason` above is typed.

`.catch` returns a **plain REC** — its declared failures discharged — so you `yield*` or `mount` it like
any other. A REC that cannot fail with a tagged error takes `.catch({})`.

## Errors and loading are separate channels

A REC's type carries failures in `E` and loading in `S`, and they discharge independently: `.catch`
handles the errors, [`.suspense`](/docs/loading/) handles the loading. The `query` above fetches async
data, so `Profile` carries **both** — chain the two, declaratively:

```tsx
const Profile = rec(function* () {
  const user = yield* query(fetchUser(id), id);
  return <Card user={user} />;
})
  .catch({ NotFound: () => <Empty />, Unauthorized: () => <Login /> }) // errors → UI
  .suspense(<Spinner />); // loading → UI
```

## Sync and async, uniformly

A failing effect surfaces the same tagged error whether it failed synchronously (a bare `yield* effect`,
thrown during render) or asynchronously (a `query`, re-thrown once its promise settles). `.catch`
renders the matching fallback either way. During an async wait the component suspends, so its
[`.suspense`](/docs/loading/) boundary — or a plain `<Suspense>` above it — shows the loading fallback
until the value, or the failure, arrives.

## Recovery is automatic

`.catch` renders the fallback in place of the component, and **recovers on its own** when the inputs that
failed change — no retry button, no reset key to wire up:

```tsx
const ProfileView = rec(function* () {
  const id = yield* session.userId; // a service atom
  const user = yield* query(fetchUser(id), id); // fails with NotFound for id 2
  return <Panel user={user} />;
}).catch({ NotFound: () => <Empty /> });

session.userId.set(2); // the query fails → <Empty/> renders in place
session.userId.set(1); // the boundary re-arms → the body re-runs, refetches, renders the Panel
```

While the fallback is shown, the boundary watches **every atom the failed body had read** — a `query`'s
key inputs among them — and resets when any of them changes, re-running the REC from the top.

The order of your `yield*`s doesn't matter — a failure part-way through the body is handled the same way
wherever the failing yield sits.

## What `.catch` does not swallow

`.catch` handles a REC's **own** `yield*`ed failures. It deliberately leaves three things alone:

- **Suspense signals** — an async yield still suspends; `.catch` never turns a pending load into an error.
- **Defects** (`Effect.die`, thrown exceptions) — these are bugs, not typed failures, so they pass
  through to the nearest React error boundary (in dev, enriched with the failing REC's name and the
  pretty-printed `Cause`).
- **A child REC's failures** — a child owns its own errors. Wrap the child in `.catch` too; a parent
  never silently swallows what a child failed to handle. This holds identically on the client and the
  server.

## `.catchDefect` — the explicit last resort

Sometimes a component must stay standing no matter what — a widget in a dashboard, a third-party
island. `.catchDefect(handler)` is the opt-in for everything `.catch` cannot name: a defect, a
non-tagged failure, a tag the map didn't list at runtime.

```tsx
const Widget = rec(function* () {
  const data = yield* query(load, key); // may die on a bug, not just fail
  return <Chart data={data} />;
})
  .catch({ NotFound: () => <Empty /> }) // typed failures first — tags always win
  .catchDefect((defect, { retry }) => <ErrorCard onRetry={retry} />);
```

It is deliberately **not** a silent catch-all: the handler receives the original thrown value, recovery
works exactly like `.catch` (atom-watch reset + `retry`), and in development every absorbed defect is
reported through the [devtools seam](/docs/devtools/) (`kind: 'defect:handled'`, with the full pretty
`Cause`) — a swallowed bug stays visible while you build. It returns a REC whose error channel is fully
discharged (`E = never`). Without it, nothing changes: defects propagate, as above.

## On the server

`.catch` and `.catchDefect` are the same values and the same API in a React Server Component graph — the
server `mount` honours them while driving the tree (tags first, the defect fallback last), so a
service-only REC renders its typed fallback on the server with no client JavaScript. Nothing to import,
nothing to change.
