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, becausegetDerivedStateFromErrorandcomponentDidCatchhave no hook equivalent. React 19 addsonCaughtError,onUncaughtErrorandonRecoverableErroroncreateRoot, which log rather than render and are silently ignored on React 18.
Detailed Resources:
- examples/core.md — the class boundary,
react-error-boundaryusage,resetKeys,useErrorBoundary, granular placement - examples/react-19-hooks.md —
createRoot/hydrateRooterror options,captureOwnerStack(), error filtering - examples/recovery.md — retry limits, exponential backoff, error classification
- examples/testing.md — what makes a boundary testable, and the fixtures that do it
- reference.md — what boundaries catch, lifecycle and prop tables, checklists
Which path applies
- No boundary dependency wanted — write the class yourself;
getDerivedStateFromErrorpluscomponentDidCatchis the whole API, and examples/core.md has it in full. react-error-boundaryis available — takeresetKeys,useErrorBoundaryand theFallbackPropstype rather than reimplementing them, and follow examples/core.md.- React 19, and the question is logging rather than UI — the three
createRootoptions report every error including the ones no boundary caught; see 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 —
onErrorand 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.
<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.
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
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.
<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; full code in examples/core.md.
Pattern 3: showBoundary() for async failures
An async throw never reaches a boundary on its own. useErrorBoundary hands you the trigger.
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
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.
<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.
<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
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.
<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
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.
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
</patterns><red_flags>
Red flags
Breaks at runtime:
- Side effects in
getDerivedStateFromError— it runs during render, so a fetch or asetStatethere breaks React's phase rules — report fromcomponentDidCatchinstead. - 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
resetKeysentry — 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.
onCaughtErrorruns after the boundary's owncomponentDidCatch, not before it.onRecoverableErroroften carries the original throw onerror.cause.captureOwnerStack()returnsnulloutside development.- The three root options are silently ignored on React 18 — no warning, no error, no logging.
</red_flags>
Scan to join WeChat group