web-error-handling-error-boundaries · git:20260906.3dc53ce · 2026-09-06 · sha256 19fc997dca73c311
web-error-handling-error-boundaries git:20260906.3dc53ceA
Immutable. This exact content is served forever at /api/v1/blob/19fc997dca73c311.
---
name: web-error-handling-error-boundaries
description: Error boundary patterns, fallback UI, reset/retry, react-error-boundary library, React 19 createRoot error hooks
---
# React Error Boundaries
> **Quick Guide:** A boundary catches errors thrown during render, in lifecycle methods and in constructors, and swaps the subtree for fallback UI. It never sees event-handler, async or server-render errors — those reach it only when something calls `showBoundary()`. Boundaries are class components, because `getDerivedStateFromError` and `componentDidCatch` have no hook equivalent. React 19 adds `onCaughtError`, `onUncaughtError` and `onRecoverableError` on `createRoot`, which log rather than render and are silently ignored on React 18.
**Detailed Resources:**
- [examples/core.md](examples/core.md) — the class boundary, `react-error-boundary` usage, `resetKeys`, `useErrorBoundary`, granular placement
- [examples/react-19-hooks.md](examples/react-19-hooks.md) — `createRoot`/`hydrateRoot` error options, `captureOwnerStack()`, error filtering
- [examples/recovery.md](examples/recovery.md) — retry limits, exponential backoff, error classification
- [examples/testing.md](examples/testing.md) — what makes a boundary testable, and the fixtures that do it
- [reference.md](reference.md) — what boundaries catch, lifecycle and prop tables, checklists
---
## Which path applies
- **No boundary dependency wanted** — write the class yourself; `getDerivedStateFromError` plus
`componentDidCatch` is the whole API, and [examples/core.md](examples/core.md) has it in full.
- **`react-error-boundary` is available** — take `resetKeys`, `useErrorBoundary` and the
`FallbackProps` type rather than reimplementing them, and follow
[examples/core.md](examples/core.md).
- **React 19, and the question is logging rather than UI** — the three `createRoot` options report
every error including the ones no boundary caught; see
[examples/react-19-hooks.md](examples/react-19-hooks.md).
---
<critical_requirements>
## Before writing error boundary code
**Return new state from `getDerivedStateFromError` and put every side effect in `componentDidCatch`.** The first runs during render, where a fetch or a log call breaks React's phase rules; the second runs at commit, where they are safe.
**Wrap each feature area in its own boundary as well as the root.** A single root boundary turns one failing widget into a blank page, and the fallback can say what failed only when it sits beside the thing that failed.
**Give the fallback a way back — a reset callback, `resetKeys`, or both.** Without one the only recovery a user has is a full page reload, which costs them everything they had typed.
**Put `role="alert"` on the fallback and make its controls real buttons.** The subtree vanishing is silent otherwise, and a screen reader user gets no announcement that anything went wrong.
**Route async and event-handler failures through `showBoundary()`.** A boundary cannot see a rejected promise, so an unhandled one leaves the UI showing stale content with no error state at all.
</critical_requirements>
---
**Auto-detection:** error boundary, ErrorBoundary, getDerivedStateFromError, componentDidCatch, fallback UI, react-error-boundary, useErrorBoundary, showBoundary, error fallback, onCaughtError, onUncaughtError, onRecoverableError, captureOwnerStack, FallbackProps, resetKeys
**Applies to:**
- Catching render-phase errors and showing fallback UI in their place
- Reset and retry after a caught error, including retry limits and backoff
- Deciding where boundaries go and how coarse each one should be
- Centralised error reporting from the React root
**Handled elsewhere:**
- Errors thrown in server rendering — the rendering framework decides what a failed render sends to the client, and no client boundary is mounted yet.
- Request failures in a data layer — a boundary sees them only if something rethrows or calls `showBoundary()`; retry and cache invalidation belong to whatever fetches.
- Field-level validation feedback — an invalid form field is expected input, so it renders inline rather than replacing the subtree.
- The monitoring destination — `onError` and the root handlers hand you an error and a component stack, and where those go is the reporting tool's concern.
---
<philosophy>
## Philosophy
A render error that no boundary catches unmounts the entire tree: React tears the root down rather
than leave a half-rendered document on screen, so one thrown error anywhere becomes a blank page.
A boundary buys **isolation** against that: the blast radius is the subtree the nearest boundary
wraps, so where the boundaries sit decides how much of the page a single bug costs. That makes
placement the real decision — recovery, fallback wording and logging all follow from it.
Boundaries do not replace `try`/`catch`; they cover the one region `try`/`catch` cannot reach, which
is React's own render.
</philosophy>
---
<patterns>
## Core patterns
### Pattern 1: Class-based boundary
The two lifecycle methods split by phase: `getDerivedStateFromError` is pure and returns state,
`componentDidCatch` is where reporting goes.
```typescript
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.props.onError?.(error, errorInfo);
}
```
Full code: [examples/core.md](examples/core.md)
### Pattern 2: `react-error-boundary`
A `FallbackComponent` receives `error` and `resetErrorBoundary`, so the retry control lives wherever
the design wants it. `onError` keeps reporting out of the fallback.
```typescript
<ErrorBoundary FallbackComponent={ErrorFallback} onError={report}>
<Dashboard />
</ErrorBoundary>;
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
```
The prop table is in [reference.md](reference.md); full code in [examples/core.md](examples/core.md).
### Pattern 3: `showBoundary()` for async failures
An async throw never reaches a boundary on its own. `useErrorBoundary` hands you the trigger.
```typescript
const { showBoundary } = useErrorBoundary();
const load = async () => {
try {
const response = await fetch(endpoint);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
} catch (error) {
showBoundary(error);
}
};
```
Use it where the failure should replace the subtree. Leave it alone where the error has inline UI of
its own, such as a field-level validation message.
Full code: [examples/core.md](examples/core.md)
### Pattern 4: `resetKeys` for automatic reset
Changing any listed value clears the error and re-renders the children, which is what navigation and
record-switching want.
```typescript
<ErrorBoundary FallbackComponent={ErrorFallback} resetKeys={[currentPath]}>
<PageContent path={currentPath} />
</ErrorBoundary>
```
Comparison is shallow, so an object or array key needs a stable reference or the boundary resets on
every render.
### Pattern 5: Granular placement
Each widget gets its own boundary and its own fallback text; the root boundary catches whatever the
inner ones do not.
```typescript
<ErrorBoundary fallback={<p>Chart unavailable</p>} onError={report}>
<ChartWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Table unavailable</p>} onError={report}>
<DataTable />
</ErrorBoundary>
```
One boundary around all three widgets means the first failure takes the other two with it.
There is an upper bound: the unit is the feature area a user would recognise as having failed on its
own. A boundary around every component adds class components and fallback text nobody reads, and
splits one failure into a page of small broken panels rather than one honest message.
Full code: [examples/core.md](examples/core.md)
### Pattern 6: Fallback UI
The fallback is UI a user meets at their worst moment: announce it, offer a way out, and keep the
stack trace for development.
```typescript
<div role="alert">
<h2>Something went wrong</h2>
{isDevelopment && <pre>{error.message}</pre>}
<button onClick={resetErrorBoundary}>Try again</button>
</div>
```
A `<span onClick>` retry is unreachable by keyboard, and a raw stack in production leaks internals.
Full code: [examples/core.md](examples/core.md)
### Pattern 7: React 19 root error options
`createRoot` takes three handlers that report rather than render, including for errors no boundary
caught. They complement boundaries rather than replacing them.
```typescript
const root = createRoot(container, {
onCaughtError: (error, info) => report("caught", error, info.componentStack),
onUncaughtError: (error, info) =>
report("uncaught", error, info.componentStack),
onRecoverableError: (error, info) =>
report("recoverable", error, info.componentStack),
});
```
Full code: [examples/react-19-hooks.md](examples/react-19-hooks.md)
</patterns>
---
<red_flags>
## Red flags
**Breaks at runtime:**
- Side effects in `getDerivedStateFromError` — it runs during render, so a fetch or a `setState` there breaks React's phase rules — report from `componentDidCatch` instead.
- A boundary wrapping itself — a boundary never catches its own render error, and the throw escapes to the parent boundary or to the root — keep the fallback trivial.
- An unstable `resetKeys` entry — the shallow compare sees a new array or object every render and resets the boundary continuously — memoise the value or key on a primitive.
- `<span onClick={reset}>` as the retry control — not focusable and not activated by Enter or Space — use `<button>`.
**Surprising behaviour:**
- Async and event-handler throws never reach a boundary; without `showBoundary()` they vanish.
- The innermost boundary wins, so a wide root fallback appears only when every inner one was missed.
- SSR hydration errors surface as recoverable rather than caught, and a client boundary may never see them.
- Development hot reload trips boundaries that production never would.
- `onCaughtError` runs after the boundary's own `componentDidCatch`, not before it.
- `onRecoverableError` often carries the original throw on `error.cause`.
- `captureOwnerStack()` returns `null` outside development.
- The three root options are silently ignored on React 18 — no warning, no error, no logging.
</red_flags>