# Error boundaries

> A boundary catches the errors its catchTags name, by tag, with a handler per tag typed by the error with that tag, and the reads under it are typed without them.

`effect-atom-boundary` gives Effect Atom typed [error boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary). An atom's `AsyncResult` carries its error type, such as `IssueNotFound | Unauthorized | RpcClientError` for an [`AtomRpc`](https://github.com/Effect-TS/effect/blob/effect%404.0.6/packages/effect/src/reactivity/AtomRpc.ts) query: the errors its procedure declares, and Effect RPC's [`RpcClientError`](https://github.com/Effect-TS/effect/blob/effect%404.0.6/packages/effect/src/rpc/RpcClientError.ts), which every procedure fails with when the request doesn't reach the server or the answer doesn't decode. A boundary catches those failures by `_tag`: its `catchTags` keys come from the atom's error type, so UI code never imports an error class, and a misspelled tag doesn't compile.

It needs only `effect`, [`@effect/atom-react`](https://github.com/Effect-TS/effect/tree/effect%404.0.6/packages/atom/react) and React. It works the same with or without `raf-effect-atom`.

The idea comes from [result-rpc](https://result-rpc.com/), whose [Shells](https://result-rpc.com/concepts/shells/) are error boundaries that claim failures and remove them from the component's error type. `effect-atom-boundary` is that idea over Effect Atom, claiming by `_tag`.

## A boundary names the errors it catches

`createErrorBoundary` makes a boundary from its handlers, one per `_tag` it catches: a tag with a handler is caught, and a tag without one isn't. `issue`'s error is `IssueNotFound | Unauthorized | RpcClientError`. Here `<AppBoundary>` catches `RpcClientError`, and `<IssueBoundary>`, under it, catches `IssueNotFound`, so the read under them is left with `Unauthorized`:

```tsx
export const [AppBoundary] = createErrorBoundary({
	name: "App",
	errors: issue,
	catchTags: {
		RpcClientError: ({ reset }) => (
			<p>
				Can't reach the server. <button onClick={reset}>Try again</button>
			</p>
		),
	},
});

export const [IssueBoundary, useIssueBoundary] = createErrorBoundary({
	name: "Issue",
	errors: issue,
	parent: AppBoundary,
	catchTags: {
		IssueNotFound: ({ error, reset }) => (
			<p>
				There's no issue #{error.id}. <button onClick={reset}>Try again</button>
			</p>
		),
	},
});

export function IssuePanel(props: { issue: Handle<typeof issue> }) {
	return (
		<AppBoundary>
			<IssueBoundary>
				<Suspense fallback={<p>Loading…</p>}>
					<IssueTitle issue={props.issue} />
				</Suspense>
			</IssueBoundary>
		</AppBoundary>
	);
}

function IssueTitle(props: { issue: Handle<typeof issue> }) {
	const result = useIssueBoundary(useAtomResult(issue, props.issue));
	if (AsyncResult.isSuccess(result)) {
		return <h1>{result.value.title}</h1>;
	}
	const error = Option.getOrThrow(AsyncResult.error(result));
	// const error: Unauthorized
	return Match.valueTags(error, {
		Unauthorized: () => <p>Sign in to see this issue.</p>,
	});
}
```

- `catchTags` is what the boundary catches. Its keys are tags of `errors`' error type.
- `errors` names what the errors come from, for the types only: here the `issue` atom family. It can also be an atom, or an `AtomRpc` client for the errors of all its procedures.
- `name` names the boundary in messages and React DevTools: `"Issue"` is `<IssueBoundary>`.
- `parent` puts `<IssueBoundary>` under `<AppBoundary>`, and the tags they catch add up. `<IssueBoundary>` throws when its parent isn't mounted above it.

It returns the boundary component and its hook, named by destructuring.

## Reads under it are typed without what it catches

A component under `<IssueBoundary>` passes what it reads to `useIssueBoundary`. `raf-effect-atom`'s [`useAtomResult`](https://raf-effect-atom.katt.dev/docs/reading-atoms.md) returns the typed failures, and the hook throws `IssueNotFound` to `<IssueBoundary>` and `RpcClientError` to `<AppBoundary>`, and returns any other failure, typed without the caught tags. The `^?` above shows `Unauthorized`, the one tag of `issue`'s error that neither boundary's `catchTags` names, and the component handles it by its tag.

A caught failure renders the boundary's handler in place of its children. `reset` refreshes every atom in the registry that failed with a tag the boundary handles, and renders the children again.

The handler stays until `reset`: with its children replaced, the boundary can't tell that they'd now read another atom. Give a boundary around one issue's page the issue's id as its [`key`](https://react.dev/learn/preserving-and-resetting-state#option-2-resetting-state-with-a-key), so opening another issue mounts a new boundary, without the last issue's failure.

A handler renders as a component, so it can call hooks. A handler written inline is a new component each time the component around the boundary renders, so it mounts again and loses its state: give a handler with state a name outside the render.

## When every tag is caught

When the boundaries' `catchTags` name every tag of the atom's error, no failure is left. `<IssuePageBoundary>` catches `IssueNotFound` and `Unauthorized`, and its parent, `<AppBoundary>`, catches `RpcClientError`, so no error is left in the result's type and the hook returns a success:

```tsx
export const [AppBoundary] = createErrorBoundary({
	name: "App",
	errors: issue,
	catchTags: {
		RpcClientError: ({ reset }) => (
			<p>
				Can't reach the server. <button onClick={reset}>Try again</button>
			</p>
		),
	},
});

export const [IssuePageBoundary, useIssuePageBoundary] = createErrorBoundary({
	name: "IssuePage",
	errors: issue,
	parent: AppBoundary,
	catchTags: {
		IssueNotFound: ({ error }) => <p>There's no issue #{error.id}.</p>,
		Unauthorized: ({ reset }) => (
			<p>
				You're signed out. <button onClick={reset}>Try again</button>
			</p>
		),
	},
});

export function IssuePage(props: { issue: Handle<typeof issue> }) {
	return (
		<AppBoundary>
			<IssuePageBoundary>
				<Suspense fallback={<p>Loading…</p>}>
					<IssueTitle issue={props.issue} />
				</Suspense>
			</IssuePageBoundary>
		</AppBoundary>
	);
}

function IssueTitle(props: { issue: Handle<typeof issue> }) {
	const result = useIssuePageBoundary(useAtomResult(issue, props.issue));
	// const result: Success<Issue, never>
	return <h1>{result.value.title}</h1>;
}
```

The `never` comes from those `catchTags`: without the `Unauthorized` handler, the result's error type is `Unauthorized` again, and the component has to handle it.

> [!NOTE]
> The [web version of this page](https://raf-effect-atom.katt.dev/docs/error-boundaries) has an interactive demo here. Its code: [every-tag-caught.tsx](https://github.com/KATT/raf-effect-atom/blob/main/www/src/demos/every-tag-caught.tsx).

## Handlers are typed by their tag

Each handler's `error` is the member of the error type with that tag:

```tsx
export const [IssueAccessBoundary] = createErrorBoundary({
	name: "IssueAccess",
	errors: issue,
	catchTags: {
		IssueNotFound: ({ error }) => {
			error;
			// (parameter) error: IssueNotFound
			return <p>There's no issue #{error.id}.</p>;
		},
		Unauthorized: ({ error }) => {
			error;
			// (parameter) error: Unauthorized
			return <p>Sign in to see this issue.</p>;
		},
	},
});
```

A tag the atom can't fail with is a type error:

```tsx
export const [NotFoundBoundary] = createErrorBoundary({
	name: "NotFound",
	errors: issue,
	catchTags: {
		IssueNotFund: () => <p>Not found</p>,
	},
});
```

## What a boundary doesn't catch

- A failure whose tag has no handler goes to the error boundaries above it.
- A defect or an interruption isn't a typed error: the read throws `Cause.squash(cause)` to the app's own error boundary.

A boundary from `createErrorBoundary` matches by tag: it catches its tags from any atom read under it, not only from the results passed to its hook. [Shared boundaries](https://raf-effect-atom.katt.dev/docs/shared-boundaries.md) covers boundaries for failures that belong to the app, such as `RpcClientError` and being signed out, and chaining them with `parent`.
