Topic 14.5
Error Boundaries
In one line
An error boundary is a class component with static getDerivedStateFromError (show a fallback) and componentDidCatch (log), that catches errors thrown while rendering its children; it doesn't catch errors in event handlers or async code, it's reset with a key or a reset function, and React 19 adds root-level onCaughtError, onUncaughtError and onRecoverableError callbacks.
Think of it like this
Fuses in a house. If the toaster short-circuits, the kitchen fuse trips and the kitchen goes dark, but the rest of the house keeps its lights on. Without fuses, one faulty toaster could cut the power to the whole building. An error boundary is a fuse for part of your page: when a component inside it breaks, only that part is replaced with a message, and the rest of the app keeps working.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Error boundary
- A component that catches errors thrown while rendering its children and shows a fallback instead.
- Fallback UI
- The message or screen a boundary shows in place of the broken part.
- getDerivedStateFromError
- A static class method that turns a caught error into state, so the boundary can render its fallback.
- componentDidCatch
- A class method that runs after an error is caught; used for logging.
- Component stack
- A text list of the components an error happened inside, from the broken one up to the root.
- Reset
- Clearing a boundary's error so it tries to render its children again.
- resetKeys
- Values a boundary watches; when one changes, the boundary resets itself.
- Uncaught error
- An error no boundary caught. In React 19 the whole root is unmounted.
- Recoverable error
- An error React worked around by itself, such as a failed render that succeeded on retry.
Step by step
01See what happens without a boundary
A render error with no boundary removes the entire app from the page and logs Uncaught Error: ... plus advice to add a boundary (you saw this in Topic 14.2's breakIt).
02Write the boundary class
getDerivedStateFromError decides what to show (render phase, must be pure). componentDidCatch does side effects (commit phase).
import { Component } from "react";
export class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
logToService(error, info.componentStack);
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}03Place boundaries around independent parts
One near the top as a last resort ("Something went wrong, reload"), and smaller ones around widgets that can fail alone: a chart, a comments panel, each route's page content.
04Know where errors go
Render, effect and use() errors travel up to the nearest boundary. Handler and async errors don't, unless you rethrow them into rendering.
05Reset after fixing the cause
Give the fallback a Try again button and/or watch reset keys. The third live example resets when the user picks another product, or with the button.
06Report everything from the root
Use the React 19 root options to send caught, uncaught and recoverable errors to monitoring, with the component stack. The fourth live example prints the real arguments.
createRoot(document.getElementById("root"), {
onCaughtError: (error, info) => report("caught", error, info.componentStack),
onUncaughtError: (error, info) => report("uncaught", error, info.componentStack),
onRecoverableError: (error, info) => report("recoverable", error, info.componentStack),
}).render(<App />);Try it yourself
- 1
Remove the boundary
In the first example, delete the
<ErrorBoundary>tags around<PriceChart />. Predict what the screen line shows now. (The whole app unmounts, so thescreen:line would crash onnull; that is exactly the point.) - 2
Reset with a key instead
In the third example, remove
resetKeysand write<ErrorBoundary key={id} ...>. Predict the log. A new key mounts a brand-new boundary, so picking product 2 after an error works the same way. - 3
Catch the handler error yourself
In the second example, change
SaveButtonto keep anerrorstate and wrap the throw intry/catch, showing<p>Save failed</p>. Or rethrow it with thesetState(() => { throw e })trick and watch the boundary take over.
Code & diagrams
React logs the caught error (with the component name), then calls `componentDidCatch`. Only the chart is replaced; the heading and the cart text stay.
Preview
Console
Nothing logged yet.Expected console output
TypeError: Cannot read properties of undefined (reading 'reduce')
The above error occurred in the <PriceChart> component.
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
componentDidCatch: Cannot read properties of undefined (reading 'reduce') | stack is a string
screen: Product page | Laptop, ₹55,000 | This widget failed to load. | Add to cart still works.The handler error goes to the browser as `Uncaught Error` and the boundary never sees it. The rethrown async error reaches the boundary. (`react-error-boundary`'s `showBoundary` does the same rethrow for you.)
Preview
Console
Nothing logged yet.Expected console output
Uncaught Error: click failed
after handler error, fallback shown? false
Error: async failed
The above error occurred in the <LaterButton> component.
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
after async error: Boundary: async failedPicking product 2 resets the boundary through `resetKeys`. Try again calls `onReset` (back to product 1, which works) before clearing the error, so the reset doesn't throw again.
Preview
Console
Nothing logged yet.Expected console output
logged: No product 3
click 3: Something went wrong: No product 3
click 2: Product 2: Phone
logged: No product 3
click 3: Something went wrong: No product 3
try again: Product 1: LaptopEach callback receives the error and an info object. The recoverable error wraps the original one in `error.cause`. After the uncaught error, React unmounts the whole root: the screen is empty. Because the callbacks are passed, React's default console messages are not printed.
Preview
Console
Nothing logged yet.Expected console output
onCaughtError: caught boom | boundary: Boundary | has stack: string
onRecoverableError: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root. | cause: flaky first render
onUncaughtError: uncaught boom | keys: componentStack
screen: (empty)import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
function Fallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function SaveButton() {
const { showBoundary } = useErrorBoundary();
async function save() {
try {
await api.save();
} catch (error) {
showBoundary(error); // send an async error to the nearest boundary
}
}
return <button onClick={save}>Save</button>;
}
export function ProductPage({ id }) {
return (
<ErrorBoundary
FallbackComponent={Fallback}
onError={(error, info) => logToService(error, info.componentStack)}
resetKeys={[id]}
>
<Product id={id} />
<SaveButton />
</ErrorBoundary>
);
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Expect a boundary to catch an event handler error
Throw inside onClick of a button that sits inside an error boundary.
Break #2
Reset on every change of the reset keys
In the reset example, write componentDidUpdate(prevProps) { if (this.state.error && changed) this.setState({ error: null }); } (checking the current error instead of the previous one).
Myth vs fact
Myth
Error boundaries catch every error in their subtree.
Fact
They catch render, lifecycle, effect and use() errors. Event handlers, timers, promise callbacks and server rendering need try/catch or a rethrow into render.
Myth
You can write an error boundary with hooks.
Fact
Not yet: React has no hook equivalent of getDerivedStateFromError. Use a class (once) or the react-error-boundary package.
Myth
One boundary at the top of the app is enough.
Fact
It prevents a blank screen, but a broken widget then replaces the whole app. Add boundaries around independent parts (routes, widgets) so failures stay small.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
In development, React logs caught errors with
console.error(the "The above error occurred in the <X> component" text). React 19 reports each error once (React 18 logged them twice: rethrown and reported) and moved the default logging to the root callbacks, which you can override. - ▸
React retries a failed render once synchronously before going to the boundary (that's why a
console.login a throwing component prints twice). If the retry succeeds, the error is reported toonRecoverableErrorinstead. - ▸
Since React 19, errors in
useEffectanduseLayoutEffectsetup and cleanup are sent to the nearest error boundary. In React 18 effect errors were also caught by boundaries, but the reporting was less consistent. - ▸
Boundaries interact with Suspense: a boundary above a
<Suspense>catches both render errors and rejected promises read withuse. With TanStack Query,throwOnError: truemakes queries throw into the nearest boundary, andQueryErrorResetBoundaryresets failed queries when the boundary resets (Topic 14.6).
Remember this
- 1
Why boundaries exist. If a component throws an error while rendering and nothing catches it, React 19 removes the whole app from the screen (unmounts the root) and reports the error, because showing half-broken UI could mislead the user (a wrong price, a wrong balance). An error boundary catches render errors in the tree below it and shows a fallback UI instead of that subtree, so one broken widget doesn't blank the page.
- 2
How to write one. Error boundaries are still written as class components (there is no hook for them yet; classes are covered in Topic 26.4). A class becomes a boundary when it defines either or both of: **
static getDerivedStateFromError(error)**, which runs during render and returns new state, such as{ error }, sorender()can show the fallback; and **componentDidCatch(error, info), which runs after the fallback is committed and is the place for side effects** like sending the error to a logging service.info.componentStackis a string showing which components the error passed through. You write one boundary class and reuse it everywhere with different fallbacks. - 3
What boundaries catch, and what they don't. They catch errors thrown during rendering, in lifecycle methods, in effects and their cleanups (React 19 reports effect errors to the nearest boundary too), and rejected promises read with
use()(Topic 14.4). They don't catch: errors in event handlers (a click handler runs outside rendering, so usetry/catchthere and set error state), asynchronous code (setTimeout, promise callbacks after the event), errors in the boundary itself (only a boundary above it can catch those), and errors during server rendering. A common trick for async errors: rethrow inside a state updater,setState(() => { throw error; }), which turns it into a render error that the nearest boundary catches. - 4
Resetting. After a boundary shows its fallback, it stays in the error state until something resets it. Three ways: (1) a "Try again" button that clears the boundary's state (
this.setState({ error: null })), usually after fixing the cause (refetching, resetting a value); (2) reset keys: the boundary resets automatically when a value it watches changes, such as the product id or the route; (3) a **key** on the boundary:<ErrorBoundary key={productId}>throws the old boundary away and mounts a fresh one when the key changes (Topic 6.4 explains keys and state). Resetting without fixing the cause just throws again. - 5
**
react-error-boundary.** Most teams don't hand-write boundaries; they install the smallreact-error-boundarypackage. Its<ErrorBoundary>takesfallback,fallbackRenderorFallbackComponent(which receiveserrorandresetErrorBoundary),onErrorfor logging,onReset, andresetKeys. It also providesuseErrorBoundary(), whoseshowBoundary(error)sends an error from an event handler or async code to the nearest boundary. The package isn't available in Hectal's preview, so the live example builds a small version with the same props. - 6
Root error callbacks (React 19).
createRoot(container, options)accepts three callbacks that see every error the root handles: **onCaughtError(error, info)** for errors caught by a boundary (info.componentStack,info.errorBoundary); **onUncaughtError(error, info)for errors no boundary caught, after which the app is unmounted; andonRecoverableError(error, info)for errors React recovered from by itself (for example, a concurrent render that failed and succeeded on a synchronous retry, or a hydration mismatch it fixed by rendering on the client, Topic 20.6). They're the right place to send errors to a monitoring service (Topic 23.5). Passing one replaces** React's default console logging for that kind of error.
Explain it without notes
What is an error boundary and how do you write one?
What's the difference between getDerivedStateFromError and componentDidCatch?
Which errors do error boundaries not catch, and how do you handle those?
How do you reset an error boundary? Compare a Try again button, reset keys and the key prop.
What are onCaughtError, onUncaughtError and onRecoverableError, and when would you use them?
Practice
Write a reusable ErrorBoundary that takes a fallback render function (error, reset) => JSX and use it around a component that throws when a prop is negative.
A DeleteButton calls await api.delete(id) in its click handler. Make a failure show in the nearest error boundary.
Configure createRoot so that caught errors are logged with console.warn and uncaught ones with console.error, each with the component stack.
Trade-offs
- ↔
Fine-grained boundaries keep failures small but add fallback designs and code. Coarse boundaries are simpler but replace more of the page on one failure. Usually: one root, one per route, and one per independent widget.
- ↔
Hand-writing a boundary avoids a dependency and teaches the mechanism, but
react-error-boundaryalready handles the edge cases (reset loops,showBoundary, typed props). Most teams use the package.
Done when you can
I can write an error boundary class with both methods.
I know which errors boundaries don't catch and how to forward them.
I can reset a boundary with a button, reset keys or a key.
I can use
react-error-boundaryand itsshowBoundary.I can configure
onCaughtError,onUncaughtErrorandonRecoverableErroron a root.