In one minute
If a component throws while rendering and nothing catches it, React removes the whole app from the page. You can’t fix that with try/catch around JSX, because JSX only creates element objects: the component runs later, inside React. An error boundary is a component that catches errors thrown while rendering anything below it and shows a fallback instead, like a catch block for a part of the tree. Most apps use the small react-error-boundary package rather than writing the class component themselves.
You’ll be able to: put boundaries in the right place, know which errors they can’t see, and recover with “Try again”.
1function ProductDetails({ product }) {2 return <p>{formatUSD(product.price.cents)}</p>3}45<main>6 <h1>Store</h1>7 <ProductDetails product={mysteryBox} />8</main>- render phase
return <p>{formatUSD(product.price.cents)}</p>The product has noprice, so this throws:Cannot read properties of undefined (reading 'cents'). - render phase
return <p>{formatUSD(product.price.cents)}</p>React renders once more to rule out a one-off glitch (recorded:ProductDetails renders p404twice). It throws again. <main>React walks up the tree looking for an error boundary. There is none.- commit phaseWith no boundary, React removes everything. Recorded:
#rootwas empty, andonUncaughtErrorfired.
1function ProductDetails({ product }) {2 return <p>{formatUSD(product.price.cents)}</p>3}45<main>6 <h1>Store</h1>7 <ErrorBoundary FallbackComponent={ErrorFallback}>8 <ProductDetails product={mysteryBox} />9 </ErrorBoundary>10 <ProductDetails product={mug} />11</main>- render phase
return <p>{formatUSD(product.price.cents)}</p>The same error is thrown, this time inside anErrorBoundary. - render phase
<ErrorBoundary FallbackComponent={ErrorFallback}>React retries once, then walks up and finds the nearest boundary. It renders the boundary’s fallback instead of its children. - commit phaseOnly that part of the page changes. Recorded screen: “Store”, “Couldn’t show this product: …”, “Try again”, and the healthy Ceramic Mug still there.
The example: a product the API sent without a price
function ProductDetails({ product }: { product: MaybeProduct }) {
log(`ProductDetails renders ${product.id}`)
return (
<article className="product-card">
<h2>{product.name}</h2>
<p className="price">{formatUSD(product.price!.cents)}</p>
</article>
)
}
Without a boundary
r.render(
<main>
<h1>Store</h1>
<ProductDetails product={broken} />
</main>,
)
What the lesson logged (the root was created with onUncaughtError / onCaughtError callbacks):
ProductDetails renders p404
ProductDetails renders p404
root: uncaught → Cannot read properties of undefined (reading 'cents')
And #root afterwards:
Empty: the heading and everything else is gone. Notice ProductDetails renders p404 twice: when a component throws, React renders it once more before giving up, in case the error was a one-off.
With a boundary
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>Couldn’t show this product: {(error as Error).message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
r.render(
<main>
<h1>Store</h1>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ProductDetails product={broken} />
</ErrorBoundary>
<ProductDetails product={fine} />
</main>,
)
The page text afterwards:
[
"Store",
"Couldn’t show this product: Cannot read properties of undefined (reading 'cents')",
"Try again",
"Ceramic Mug",
"$18.00"
]
Only the broken product was replaced. The heading and the healthy product next to it are still there.
What a boundary can’t catch
A boundary catches errors thrown while React renders (and in lifecycle methods). It can’t see errors that happen outside rendering: event handlers, setTimeout, promises. For those, useErrorBoundary().showBoundary(error) hands the error to the nearest boundary yourself:
function AddToCart() {
const { showBoundary } = useErrorBoundary()
return (
<>
<button id="unsafe" onClick={() => { throw new Error('Cart service is down') }}>
Add to cart
</button>
<button id="safe" onClick={() => {
try {
throw new Error('Cart service is down')
} catch (error) {
showBoundary(error)
}
}}>
Add to cart (reported)
</button>
</>
)
}
Clicking “Add to cart”:
{
"logs": [
"window error event → Uncaught Error: Cart service is down"
],
"warnings": [],
"fallbackShown": false
}
Clicking “Add to cart (reported)”:
{
"logs": [
"root: caught by a boundary → Cart service is down"
],
"warnings": [],
"fallbackShown": true,
"text": [
"Couldn’t show this product: Cart service is down",
"Try again"
]
}
A boundary also can’t catch an error in itself or in its fallback: those go to the next boundary up.
Trying again
resetErrorBoundary (passed to the fallback) clears the error and mounts the children again from scratch. Pair it with onReset to fix whatever caused the error:
function Page() {
const [product, setProduct] = useState(broken)
return (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => setProduct(fine)}>
<Note />
<ProductDetails product={product} />
</ErrorBoundary>
)
}
{
"logs": [
"ProductDetails renders p404",
"ProductDetails renders p404",
"root: caught by a boundary → Cannot read properties of undefined (reading 'cents')",
"ProductDetails renders p1"
],
"warnings": [],
"before": [
"Couldn’t show this product: Cannot read properties of undefined (reading 'cents')",
"Try again"
],
"after": [
"Ceramic Mug",
"$18.00"
]
}
The note input was inside the boundary, so it disappeared with the error and came back empty: a reset re-mounts, it doesn’t restore state.
Where to put boundaries
- The component that may throw must be a child of the boundary, not the component that renders the boundary. A boundary only catches errors from the tree below it.
- One boundary near the root avoids blank pages; smaller ones around independent widgets (a product, a review list, a sidebar) keep the rest of the page usable.
- Still log everything: React 19’s
createRoot(container, { onCaughtError, onUncaughtError })runs for every error, caught or not.
Interview Q&A
Why can't try/catch around JSX catch render errors?
JSX only builds element objects. The component function runs later, when React renders, long after the try block has finished. Errors thrown there are caught by React, which looks for an error boundary above the component.
Why must the component that might throw be inside the boundary?
A boundary catches errors from its descendants only. If the risky code is in the same component that renders the boundary, the error happens before the boundary exists in the tree.
What happens to the page when a render error isn't caught?
React unmounts the whole root. Recorded: #root was "" and onUncaughtError fired. With a boundary, only that subtree showed the fallback; the rest of the page stayed.
Which errors can't an error boundary catch?
Errors in event handlers, timers and async code (they don’t happen during rendering), errors in the boundary itself, and errors in its fallback. Recorded: an error thrown in an onClick reached window’s error event and no fallback appeared; with showBoundary(error) the fallback appeared.
What does resetErrorBoundary do?
It clears the error and mounts the boundary’s children again as if for the first time, so their local state is lost. Use onReset to change whatever caused the error, otherwise it will just throw again.
Why does an error boundary have to be a class component?
The APIs React uses to catch render errors, static getDerivedStateFromError and componentDidCatch, only exist on class components. Libraries like react-error-boundary wrap that class so you can use it as a regular component plus hooks.
Related
- Forms: errors thrown in event handlers and actions.
- Rendering Arrays: another way to remount a subtree, with
key.
Sources
- react.dev: Catching rendering errors with an error boundary,
createRootoptions react-error-boundary- Topic order inspired by Kent C. Dodds’ EpicReact React Fundamentals workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary