Error boundaries
Catch rendering errors with React error boundaries: what they catch, writing one as a class, retrying with a key, where to put them, and root error options.
- 01Know what a boundary catchesTell errors from rendering, Effects,
use, and transitions, which a boundary catches, from errors in handlers and timers, which it doesn't. - 02Write and reset a boundaryWrite an error boundary class with
getDerivedStateFromErrorandcomponentDidCatch, and let the reader try again with a reset or a new key. - 03Place boundaries and report errorsPut boundaries around parts that can fail on their own, and report errors with the root's
onCaughtError,onUncaughtError, andonRecoverableError.
One broken review shouldn't blank the page
Real data is messy. Here a product page lists reviews from the reviews.js tab, and the third review has no author: the shopper deleted their account. Nothing goes wrong until someone asks to see it. The result is live:
import { useState } from "react";import { reviews } from "./reviews.js"; function Reviews() { const [showAll, setShowAll] = useState(false); const shown = showAll ? reviews : reviews.slice(0, 2); return ( <> <ul> {shown.map((review) => ( <li key={review.id}>{review.author.name}: {review.text}</li> ))} </ul> <button onClick={() => setShowAll(true)}>Show all reviews</button> </> );} export default function App() { return ( <> <h3>Carrot cake, $4</h3> <button>Add to cart</button> <Reviews /> </> );}Line 6 shows the first two reviews, and line 11 reads each review's review.author.name. Click Show all reviews. Now line 11 reads null.name for the third review and throws a TypeError. Nothing catches it, so React removes the whole page from the screen, the heading and the Add to cart button included, and the preview shows the error instead. (Press Run again to get the page back.) React does this on purpose: showing a half-broken page could mislead the shopper, so it shows nothing.
An error boundary is a component that catches errors thrown while the components inside it render, and shows a fallback (a stand-in, like a short message) in their place. Here is the same page with one, from the ErrorBoundary.jsx tab:
import { useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { reviews } from "./reviews.js"; function Reviews() { const [showAll, setShowAll] = useState(false); const shown = showAll ? reviews : reviews.slice(0, 2); return ( <> <ul> {shown.map((review) => ( <li key={review.id}>{review.author.name}: {review.text}</li> ))} </ul> <button onClick={() => setShowAll(true)}>Show all reviews</button> </> );} export default function App() { return ( <> <h3>Carrot cake, $4</h3> <button>Add to cart</button> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </> );}Lines 25 to 27 wrap Reviews in ErrorBoundary, and line 25 gives it the fallback. Click Show all reviews again: the reviews are replaced by Reviews aren't available right now., and the heading and the button stay. Open the console under the result: ErrorBoundary printed Report: Cannot read properties of null (reading 'name'), and React logged the error too.
Reviews threw while rendering. The error went up to the nearest ErrorBoundary, which showed its fallback in place of its children. The heading and the button sit outside the boundary, so they stay.A house has a fuse for each room. When the kettle shorts, the kitchen's fuse trips and the kitchen goes dark, but the living room keeps its lights. Once the kettle is fixed, you reset the fuse.
- In real life: A fault in the kitchen kettle
- In React: Reviews throws while rendering
- In real life: The kitchen's fuse trips
- In React: The nearest error boundary catches the error
- In real life: The kitchen goes dark; the rest of the house keeps its lights
- In React: Only the reviews show the fallback; the page keeps working
- In real life: A house with one main fuse goes dark all at once
- In React: With no boundary, React removes the whole page
- In real life: Fix the kettle, then reset the fuse
- In React: Fix the cause, then reset the boundary
An error boundary catches errors thrown while the components inside it render, and shows a fallback instead. Without one, an error while rendering removes the whole app from the screen. With one, only the part inside the boundary is replaced, and the rest of the page keeps working.
What a boundary catches, and what it doesn't
A boundary catches errors from React's own work on the components inside it: rendering them, running their Effects, reading a promise with use, and running a transition's action. Code that runs later, on its own, isn't React's work, so the boundary never hears about its errors.
use, and transition actions. Code that runs later, on its own, like a click handler or a timer, is outside that work, and so are the boundary itself and the server.Try each place in the playground. It renders a small page with a Widget inside a boundary, throws in the place you pick, and shows who got the error: the root's onCaughtError (a boundary caught it) or its onUncaughtError (nothing caught it). The last section of this lesson explains those two options. Errors from handlers and timers aren't in the playground, because they would reach this page's own error handling; the next section shows one in a live example instead.
Pick a button below.
- onCaughtError
- not called
- onUncaughtError
- not called
Each button renders a fresh page with a Widget inside an error boundary, and throws in one place. Then it shows which of the root's options got the error.
Read the table row by row: where the error is thrown, whether a boundary above catches it, and why.
| Where the error is thrown | Caught by a boundary above? | Why |
|---|---|---|
| A component's body, while it renders | Yes. | React is rendering the tree below the boundary, so it knows which boundary is nearest. |
An Effect, like useEffect | Yes. | React runs Effects as part of its work on the tree, and knows which component each one belongs to. |
use with a rejected promise | Yes. | use throws the reason while the component renders. |
A function passed to useTransition's startTransition | Yes. | React runs it as part of the component's update and hands its error to the nearest boundary. |
An event handler, like onClick | No. | It runs after rendering, when the reader clicks. Catch the error there, or hand it to the boundary yourself. |
A timer or a promise callback, like setTimeout or .then() | No. | The browser runs it later, on its own. React isn't involved. |
| The error boundary itself, or its fallback | No. | A boundary can't catch its own errors. The next boundary up gets them. |
| Server rendering, when the page is made into HTML | No. | Boundaries only work in the browser. On the server, React sends the nearest Suspense fallback and tries again in the browser. |
The last row is about server rendering, where a framework turns your components into HTML before the page reaches the browser. Error boundaries don't run there. If a component throws on the server inside a Suspense boundary, React sends that boundary's fallback in the HTML and renders the component again in the browser; if it throws there too, the nearest error boundary catches it. The Server rendering and hydration lesson covers server rendering.
Hand an event handler's error to the boundary
Here a click handler throws, with a boundary right above its component:
import ErrorBoundary from "./ErrorBoundary.jsx"; function Coupon() { function handleApply() { throw new Error("The coupon CAKE50 has expired."); } return <button onClick={handleApply}>Apply coupon CAKE50</button>;} export default function App() { return ( <ErrorBoundary fallback={<p>Coupons can't be used right now.</p>}> <Coupon /> </ErrorBoundary> );}Line 5 throws when the handler runs. Click Apply coupon CAKE50: the boundary's fallback on line 12 never shows. React wasn't rendering when the handler ran, so the error went past the boundary to the browser as an uncaught error, and the preview reports it in place of the result. (Press Run again to get the button back.)
You can catch a handler's error yourself with try...catch. Often you'll show a message right there. When the right answer is the boundary's fallback, keep the error in state and throw it while rendering:
import { useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { applyCoupon } from "./api.js"; function Coupon() { const [error, setError] = useState(null); if (error) throw error; function handleApply() { try { applyCoupon("CAKE50"); } catch (caught) { setError(caught); } } return <button onClick={handleApply}>Apply coupon CAKE50</button>;} export default function App() { return ( <ErrorBoundary fallback={<p>Coupons can't be used right now.</p>}> <Coupon /> </ErrorBoundary> );}Line 11 calls applyCoupon from the api.js tab, which throws because the coupon has expired. Line 12 catches it in the handler, and line 13 saves it in state. Saving state renders Coupon again, and now line 7 throws the saved error while rendering, where the boundary on line 22 can catch it. Click Apply coupon CAKE50: the button is replaced by Coupons can't be used right now.
In React 19 there's a shorter way for work that happens after a click: run it as a transition with useTransition. An error thrown in the function you pass to its startTransition, even after an await, goes to the nearest boundary. (The Transitions lesson covers them in full.)
import { useTransition } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { placeOrder } from "./api.js"; function PlaceOrder() { const [isPending, startTransition] = useTransition(); function handleClick() { startTransition(async () => { await placeOrder(); }); } return ( <button onClick={handleClick} disabled={isPending}> {isPending ? "Placing the order…" : "Place order"} </button> );} export default function App() { return ( <ErrorBoundary fallback={<p>Your order didn't go through. Nothing was charged.</p>}> <PlaceOrder /> </ErrorBoundary> );}Line 6 gets isPending and startTransition. Lines 9 to 11 place the order inside a transition. Click Place order: line 16 shows Placing the order… while it waits, then placeOrder rejects after half a second, the await on line 10 throws, and the boundary on line 23 shows its fallback. This works only with the startTransition that useTransition returns. The startTransition you import from react isn't tied to a component, so its errors reach no boundary.
Write an error boundary as a class
Every component so far has been a function. An error boundary has to be a class component: a JavaScript class that extends React's Component. React has no hook for catching errors, so there's no function version. You write one boundary class and use it all over your app. Here is the ErrorBoundary.jsx tab from the examples, which runs in each of them:
import { Component } from "react";
export default class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
// A reporting service would get info.componentStack too.
console.log(`Report: ${error.message}`);
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}- Line 3 makes a class that extends
Component. A class component keeps its state inthis.state, reads its props fromthis.props, and returns what to show from arendermethod. - Line 4 starts the state with no error:
{ error: null }. - Lines 6 to 8: when a component inside throws, React calls
getDerivedStateFromErrorwith the error, while it's rendering. It returns the new state,{ error }. It'sstatic, so it belongs to the class and can't touchthis: it only returns state. - Lines 10 to 13: once the fallback is on screen, React calls
componentDidCatchwith the error and aninfoobject.info.componentStacklists the components the error came through, starting with the one that threw. This is the place to send a report, with the error and that list; here it logs one line. - Lines 15 to 18:
rendershowsthis.props.fallbackwhen there's an error, and the children otherwise.
getDerivedStateFromError while it renders, so it must only return the new state. The boundary renders again and returns its fallback. Once the fallback is on screen, React calls componentDidCatch, the place for side effects like reporting.Why two methods? getDerivedStateFromError runs during rendering, and rendering must be pure: React may call it more than once (in development, StrictMode calls it twice on purpose). componentDidCatch runs once, after the commit, so side effects like reporting belong there. In the reviews example, the console shows one Report line per caught error.
You'll meet class components in older code too. The Class components lesson covers them. For an error boundary, the file above is all you need.
Try again: reset the boundary
Once a boundary has caught an error, it keeps showing its fallback: its state still says error. To try again, something has to reset it. The simplest way is a new key. When a component's key changes, React throws the old one away, with its state, and makes a new one. Here the first stock check in api.js fails, like a busy server:
import { Suspense, use, useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { fetchStock, refetchStock } from "./api.js"; function Stock() { const left = use(fetchStock("cake")); return <p>Carrot cake: {left} left</p>;} export default function App() { const [attempt, setAttempt] = useState(1); function tryAgain() { refetchStock("cake"); setAttempt(attempt + 1); } return ( <ErrorBoundary key={attempt} fallback={<p>Couldn't check the stock. <button onClick={tryAgain}>Try again</button></p>} > <Suspense fallback={<p>Checking the stock…</p>}> <Stock /> </Suspense> </ErrorBoundary> );}Line 6 reads the stock with use, and the request fails after 0.6 seconds, so the boundary shows Couldn't check the stock. and a Try again button. Click it. Line 14 drops the failed promise from the cache, and line 15 changes attempt, the boundary's key on line 20. React makes a fresh boundary with no error, Stock asks again, and this time the answer is Carrot cake: 3 left.
A boundary can also reset itself. This version passes a reset function to its fallback:
import { Component } from "react";
export default class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error) {
console.log(`Report: ${error.message}`);
}
reset = () => {
this.setState({ error: null });
};
render() {
if (this.state.error) return this.props.fallback(this.state.error, this.reset);
return this.props.children;
}
}Lines 14 to 16 set the state back to { error: null }, and line 19 calls the fallback as a function, with the error and reset. It runs in the example below, where the cause is still there:
import ErrorBoundary from "./ErrorBoundary.jsx";import { reviews } from "./reviews.js"; function Reviews() { return ( <ul> {reviews.map((review) => ( <li key={review.id}>{review.author.name}: {review.text}</li> ))} </ul> );} export default function App() { return ( <ErrorBoundary fallback={(error, reset) => ( <p>Reviews failed: {error.message} <button onClick={reset}>Try again</button></p> )} > <Reviews /> </ErrorBoundary> );}Line 8 renders every review, so the bad one throws at once, and lines 17 to 19 show the fallback with the message. Click Try again a few times. Each click resets the boundary, the reviews render again, the same review throws again, and the console gets another Report line. Resetting only helps when something has changed: a new request, new data, or a fixed bug.
Read the table row by row: the question, then the answer for each way to reset.
| Question | A reset method | A new key |
|---|---|---|
| What changes? | The boundary's state goes back to { error: null }, so it renders its children again. | React throws away the boundary and everything inside it, and makes new ones. |
| Who starts it? | The fallback, through the reset function the boundary passes it. | Whatever owns the key, usually a parent like App. |
| Does it fix the cause? | No. If the data is still broken, the children throw again. | No. Change the cause first, like asking for the data again. |
| Use it when | The boundary can retry on its own, like a Try again button. | Something outside changes, like a new request, a new page, or a new user. |
Where to put error boundaries
You don't need a boundary around every component. Put them where a fallback makes sense to the reader: around a part of the page that can fail on its own and still leave the rest useful, like a widget, a sidebar, or a list of comments. A message where a single avatar should be would mean nothing; a page with one broken widget can still sell cake. Most apps also keep one boundary near the top, so even an unexpected error shows a friendly page instead of nothing.
Recommendations breaks, only its own boundary shows a fallback; the header, the cart, the products, and the reviews keep working.Move the boundaries in the playground, then break the recommendations and see what the shopper still has:
import ErrorBoundary from "./ErrorBoundary.jsx";
import { Cart, Header, Products, Recommendations, Reviews } from "./widgets.jsx";
export default function App() {
return (
<>
<Header />
<Cart />
<main>
<ErrorBoundary fallback={<p>Products couldn't load.</p>}>
<Products />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Reviews couldn't load.</p>}>
<Reviews />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Recommendations couldn't load.</p>}>
<Recommendations />
</ErrorBoundary>
</main>
</>
);
}workingPick where the error boundaries go, then press Break the recommendations. The page starts again whenever you pick.
Report errors from the root
Every React app starts with createRoot (Setting up a React project shows the main.jsx file that calls it). React 19 added two options to it, onCaughtError and onUncaughtError, next to the older onRecoverableError. They tell you about every error in one place, which is where you send errors to a reporting service. This example calls createRoot itself, so it runs as it is:
import { StrictMode, useState } from "react";import { createRoot } from "react-dom/client";import ErrorBoundary from "./ErrorBoundary.jsx"; function Header() { const [broken, setBroken] = useState(false); if (broken) throw new Error("The header broke."); return <button onClick={() => setBroken(true)}>Break the header</button>;} function Reviews() { const [broken, setBroken] = useState(false); if (broken) throw new Error("The reviews broke."); return <button onClick={() => setBroken(true)}>Break the reviews</button>;} function App() { return ( <> <Header /> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </> );} const root = createRoot(document.getElementById("root"), { onCaughtError(error, errorInfo) { console.log(`Caught by a boundary: ${error.message}`); }, onUncaughtError(error, errorInfo) { console.log(`Not caught, so the app is gone: ${error.message}`); }, onRecoverableError(error, errorInfo) { console.log(`React recovered: ${error.cause?.message}`); },}); root.render( <StrictMode> <App /> </StrictMode>,);Lines 28 to 38 create the root with three options. Each gets the error and an errorInfo object, whose componentStack says where it happened. Click Break the reviews: the boundary on line 21 catches the error, and the console shows Caught by a boundary: The reviews broke. from line 30, then the boundary's own Report line. Now click Break the header. Header sits outside every boundary, so React removes the whole app and line 33 prints Not caught, so the app is gone: The header broke.
onRecoverableError is for errors React fixed by itself. When a render throws, React tries the render once more before it goes to a boundary. If the second try works, nothing is shown to the reader, and React tells this option:
import { StrictMode, useState } from "react";import { createRoot } from "react-dom/client"; let tries = 0; function GiftNote() { tries += 1; // Fails only the first time it renders, like a value that wasn't ready yet. if (tries === 1) throw new Error("The gift note wasn't ready."); return <p>Gift note: Happy birthday!</p>;} function App() { const [gift, setGift] = useState(false); return ( <> <button onClick={() => setGift(true)}>Add a gift note</button> {gift && <GiftNote />} </> );} createRoot(document.getElementById("root"), { onRecoverableError(error) { console.log(`React recovered: ${error.cause?.message}`); },}).render( <StrictMode> <App /> </StrictMode>,);Line 9 throws on the first render of GiftNote only. Click Add a gift note: the note appears, and the console shows React recovered: The gift note wasn't ready. React wraps the original error in its own, so line 25 reads error.cause. In real apps, this option mostly reports pages whose server HTML didn't match what the browser rendered.
Read the table row by row: the option, when React calls it, and what React does when you leave it out.
| Option | React calls it when | Without it, React |
|---|---|---|
onCaughtError | An error boundary caught an error. | Logs the error with console.error, with the component it came from. |
onUncaughtError | No boundary caught an error, so React removed the whole app from the root. | Reports it to the browser as an uncaught error, and in development warns you to add a boundary. |
onRecoverableError | React hit an error but fixed it by itself, for example by rendering once more. | Reports it to the browser as an uncaught error. |
You can read those defaults in React's source: defaultOnCaughtError, defaultOnUncaughtError, and defaultOnRecoverableError, in ReactFiberErrorLogger.js. In development, the default for caught errors adds React will try to recreate this component tree from scratch using the error boundary you provided to the log.
These options run after React has finished with the error, outside any component. Use them to send a report, not to change what the page shows; the boundaries' fallbacks do that.
The react-error-boundary package
Most apps don't write their own boundary class. The react-error-boundary package gives you a ready ErrorBoundary and a hook to go with it. It isn't part of these examples, so this code doesn't run here. (Checked in October 2026: its current version, 6.1.6, works with React 18 and 19.)
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
function Coupon() {
const { showBoundary } = useErrorBoundary();
function handleApply() {
try {
applyCoupon("CAKE50");
} catch (error) {
showBoundary(error);
}
}
return <button onClick={handleApply}>Apply coupon CAKE50</button>;
}
export default function App() {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<p>{error.message} <button onClick={resetErrorBoundary}>Try again</button></p>
)}
onError={(error, info) => reportToServer(error, info.componentStack)}
>
<Coupon />
</ErrorBoundary>
);
}- Line 4:
useErrorBoundarygivesshowBoundary. Line 9 hands the handler's error to the nearest boundary with it, the same job the state andthrowdid in the coupon example. - Lines 18 to 20:
fallbackRendergets the error andresetErrorBoundary, the package'sreset. - Line 21:
onErrorplays the part ofcomponentDidCatch, for reports.
It also takes resetKeys, a list of values that reset the boundary when one of them changes, like a key that only resets the boundary. The package is built on the same class methods you wrote above, so it catches exactly what they catch.
A product page that keeps working
Here the page from the overview gets a boundary per widget: the stock check can fail and be retried, and the reviews can break, and the heading and Add to cart stay through all of it.
import { Suspense, use, useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { fetchStock, refetchStock } from "./api.js";import { reviews } from "./reviews.js"; function Stock() { const left = use(fetchStock("cake")); return <p>Carrot cake: {left} left</p>;} function Reviews() { const [showAll, setShowAll] = useState(false); const shown = showAll ? reviews : reviews.slice(0, 2); return ( <> <ul> {shown.map((review) => ( <li key={review.id}>{review.author.name}: {review.text}</li> ))} </ul> <button onClick={() => setShowAll(true)}>Show all reviews</button> </> );} export default function App() { const [stockTry, setStockTry] = useState(1); function checkAgain() { refetchStock("cake"); setStockTry(stockTry + 1); } return ( <> <h3>Carrot cake, $4</h3> <button>Add to cart</button> <ErrorBoundary key={stockTry} fallback={<p>Stock unknown. <button onClick={checkAgain}>Check again</button></p>} > <Suspense fallback={<p>Checking the stock…</p>}> <Stock /> </Suspense> </ErrorBoundary> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </> );}- Lines 38 to 45: the stock has its own boundary, keyed by
stockTryon line 39. When the first check fails, line 40 showsStock unknown.and Check again. - Lines 29 to 32:
checkAgaindrops the failed answer and changes the key, so a fresh boundary asks again and showsCarrot cake: 3 left. - Lines 46 to 48: the reviews have a boundary of their own, so Show all reviews replaces only them.
Wait for the stock check to fail, click Check again, then Show all reviews. The console shows a Report line for each of the two caught errors.
throwin a component's body, while it rendersthrowin anonClickhandlerthrowinsideuseEffectthrowin asetTimeoutcallbackuse(promise)with a promise that rejectsthrowin a.then()callback, after the data arrivesthrowin a function passed touseTransition'sstartTransitionthrowin the error boundary's ownrender
Each card is a place an error is thrown, inside a component that has an error boundary above it. Sort it: does the boundary catch the error?
Common mix-ups
- "An error boundary catches every error in the components inside it." No. It catches errors from rendering, Effects,
use, anduseTransitionactions. Errors in event handlers, timers, and promise callbacks go past it. - "A boundary replaces only the component that threw." No. It replaces everything inside it with its fallback. Wrap smaller parts to lose less.
- "I can write an error boundary as a function component." No. React 19 has no hook for it, so a boundary is a class. Write one, or use react-error-boundary.
- "A boundary tries again by itself." No. It keeps its fallback until you reset its state or give it a new key, and resetting helps only if the cause changed.
- "getDerivedStateFromError is a good place to send the report." No. It runs while rendering and may run more than once. Report from
componentDidCatchor the root'sonCaughtError. - "A boundary catches its own errors." No. An error in the boundary or its fallback goes to the next boundary up.
Practice exercises
Each exercise has a live result: click in it, change the code with Edit & run, and open the console to see the reports.
Predict first: you click Break the reviews, then Break the header. What's the last line the console prints? Then try it.
import { StrictMode, useState } from "react";import { createRoot } from "react-dom/client";import ErrorBoundary from "./ErrorBoundary.jsx"; function Header() { const [broken, setBroken] = useState(false); if (broken) throw new Error("The header broke."); return <button onClick={() => setBroken(true)}>Break the header</button>;} function Reviews() { const [broken, setBroken] = useState(false); if (broken) throw new Error("The reviews broke."); return <button onClick={() => setBroken(true)}>Break the reviews</button>;} function App() { return ( <> <Header /> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </> );} const root = createRoot(document.getElementById("root"), { onCaughtError(error, errorInfo) { console.log(`Caught by a boundary: ${error.message}`); }, onUncaughtError(error, errorInfo) { console.log(`Not caught, so the app is gone: ${error.message}`); }, onRecoverableError(error, errorInfo) { console.log(`React recovered: ${error.cause?.message}`); },}); root.render( <StrictMode> <App /> </StrictMode>,);import { StrictMode, useState } from "react";import { createRoot } from "react-dom/client";import ErrorBoundary from "./ErrorBoundary.jsx"; function Header() { const [broken, setBroken] = useState(false); if (broken) throw new Error("The header broke."); return <button onClick={() => setBroken(true)}>Break the header</button>;} function Reviews() { const [broken, setBroken] = useState(false); if (broken) throw new Error("The reviews broke."); return <button onClick={() => setBroken(true)}>Break the reviews</button>;} function App() { return ( <> <Header /> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </> );} const root = createRoot(document.getElementById("root"), { onCaughtError(error, errorInfo) { console.log(`Caught by a boundary: ${error.message}`); }, onUncaughtError(error, errorInfo) { console.log(`Not caught, so the app is gone: ${error.message}`); }, onRecoverableError(error, errorInfo) { console.log(`React recovered: ${error.cause?.message}`); },}); root.render( <StrictMode> <App /> </StrictMode>,);Breaking the reviews is caught, so line 30 prints Caught by a boundary. The header has no boundary above it, so React removes the app and line 33 prints Not caught, so the app is gone: The header broke. last.
Clicking Show all reviews removes the whole page. Make it replace only the reviews. With your fix, click it: does the Add to cart button stay?
import { useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { reviews } from "./reviews.js"; function Reviews() { const [showAll, setShowAll] = useState(false); const shown = showAll ? reviews : reviews.slice(0, 2); return ( <> <ul> {shown.map((review) => ( <li key={review.id}>{review.author.name}: {review.text}</li> ))} </ul> <button onClick={() => setShowAll(true)}>Show all reviews</button> </> );} export default function App() { return ( <> <h3>Carrot cake, $4</h3> <button>Add to cart</button> <Reviews /> </> );}import { useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { reviews } from "./reviews.js"; function Reviews() { const [showAll, setShowAll] = useState(false); const shown = showAll ? reviews : reviews.slice(0, 2); return ( <> <ul> {shown.map((review) => ( <li key={review.id}>{review.author.name}: {review.text}</li> ))} </ul> <button onClick={() => setShowAll(true)}>Show all reviews</button> </> );} export default function App() { return ( <> <h3>Carrot cake, $4</h3> <button>Add to cart</button> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </> );}With the boundary around Reviews, the error from the bad review is caught there. The fallback takes the reviews' place, and the heading and Add to cart, outside the boundary, stay.
The boundary in the ErrorBoundary.jsx tab shows its fallback but tells no one. Make it log Report: and the error's message. Then click Show all reviews. What does your line say?
import { useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { reviews } from "./reviews.js"; function Reviews() { const [showAll, setShowAll] = useState(false); const shown = showAll ? reviews : reviews.slice(0, 2); return ( <> <ul> {shown.map((review) => ( <li key={review.id}>{review.author.name}: {review.text}</li> ))} </ul> <button onClick={() => setShowAll(true)}>Show all reviews</button> </> );} export default function App() { return ( <> <h3>Carrot cake, $4</h3> <button>Add to cart</button> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </> );}import { Component } from "react"; export default class ErrorBoundary extends Component { state = { error: null }; static getDerivedStateFromError(error) { return { error }; } componentDidCatch(error, info) { // A reporting service would get info.componentStack too. console.log(`Report: ${error.message}`); } render() { if (this.state.error) return this.props.fallback; return this.props.children; }}React calls componentDidCatch once, after the fallback is on screen, with the error and info.componentStack. Logging error.message prints Report: Cannot read properties of null (reading 'name').
The stock check fails the first time, and Try again does nothing. Fix it. After you click Try again, what does the page show once the answer comes?
import { Suspense, use, useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { fetchStock, refetchStock } from "./api.js"; function Stock() { const left = use(fetchStock("cake")); return <p>Carrot cake: {left} left</p>;} export default function App() { const [attempt, setAttempt] = useState(1); function tryAgain() { refetchStock("cake"); setAttempt(attempt + 1); } return ( <ErrorBoundary fallback={<p>Couldn't check the stock. <button onClick={tryAgain}>Try again</button></p>} > <Suspense fallback={<p>Checking the stock…</p>}> <Stock /> </Suspense> </ErrorBoundary> );}import { Suspense, use, useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { fetchStock, refetchStock } from "./api.js"; function Stock() { const left = use(fetchStock("cake")); return <p>Carrot cake: {left} left</p>;} export default function App() { const [attempt, setAttempt] = useState(1); function tryAgain() { refetchStock("cake"); setAttempt(attempt + 1); } return ( <ErrorBoundary key={attempt} fallback={<p>Couldn't check the stock. <button onClick={tryAgain}>Try again</button></p>} > <Suspense fallback={<p>Checking the stock…</p>}> <Stock /> </Suspense> </ErrorBoundary> );}tryAgain dropped the failed answer and changed attempt, but the boundary kept its error, so the fallback stayed. With key={attempt}, the new number makes a fresh boundary; Stock asks again and shows Carrot cake: 3 left.
Clicking Apply coupon CAKE50 only logs a Lost: line. Make the boundary's fallback show instead. What does the page show after the click?
import ErrorBoundary from "./ErrorBoundary.jsx";import { applyCoupon } from "./api.js"; function Coupon() { function handleApply() { try { applyCoupon("CAKE50"); } catch (caught) { console.log(`Lost: ${caught.message}`); } } return <button onClick={handleApply}>Apply coupon CAKE50</button>;} export default function App() { return ( <ErrorBoundary fallback={<p>Coupons can't be used right now.</p>}> <Coupon /> </ErrorBoundary> );}import { useState } from "react";import ErrorBoundary from "./ErrorBoundary.jsx";import { applyCoupon } from "./api.js"; function Coupon() { const [error, setError] = useState(null); if (error) throw error; function handleApply() { try { applyCoupon("CAKE50"); } catch (caught) { setError(caught); } } return <button onClick={handleApply}>Apply coupon CAKE50</button>;} export default function App() { return ( <ErrorBoundary fallback={<p>Coupons can't be used right now.</p>}> <Coupon /> </ErrorBoundary> );}Saving the error renders Coupon again, and if (error) throw error; throws it while rendering, where the boundary catches it and shows Coupons can't be used right now.
Report every error a boundary catches, from one place, with a line that starts with Reported:. Then click Break the reviews. What does your line say?
import { StrictMode, useState } from "react";import { createRoot } from "react-dom/client";import ErrorBoundary from "./ErrorBoundary.jsx"; function Reviews() { const [broken, setBroken] = useState(false); if (broken) throw new Error("The reviews broke."); return <button onClick={() => setBroken(true)}>Break the reviews</button>;} const root = createRoot(document.getElementById("root")); root.render( <StrictMode> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </StrictMode>,);import { StrictMode, useState } from "react";import { createRoot } from "react-dom/client";import ErrorBoundary from "./ErrorBoundary.jsx"; function Reviews() { const [broken, setBroken] = useState(false); if (broken) throw new Error("The reviews broke."); return <button onClick={() => setBroken(true)}>Break the reviews</button>;} const root = createRoot(document.getElementById("root"), { onCaughtError(error) { console.log(`Reported: ${error.message}`); },}); root.render( <StrictMode> <ErrorBoundary fallback={<p>Reviews aren't available right now.</p>}> <Reviews /> </ErrorBoundary> </StrictMode>,);onCaughtError runs for every error any boundary in the root catches, so one option reports them all. Clicking Break the reviews logs Reported: The reviews broke.
Check your understanding
Question 1 of 7You click Apply coupon. What does the page show?
Read the code, then predictJSXfunction Coupon() { return ( <button onClick={() => { throw new Error("Expired"); }}> Apply coupon </button> ); } // In App: <ErrorBoundary fallback={<p>Coupons are off.</p>}> <Coupon /> </ErrorBoundary>Choose an answer to see the explanation.
Question 2 of 7
Reviewsthrows while rendering. What happens toHeader?Read the code, then predictJSX<ErrorBoundary fallback={<p>Something went wrong.</p>}> <Header /> <Reviews /> {/* throws while rendering */} </ErrorBoundary>Choose an answer to see the explanation.
Question 3 of 7Which of these errors does an error boundary above the component catch?
Choose an answer to see the explanation.
Question 4 of 7Why must
getDerivedStateFromErroronly return the new state, whilecomponentDidCatchmay send a report?Choose an answer to see the explanation.
Question 5 of 7A boundary shows its fallback. How do you show its children again?
Choose an answer to see the explanation.
Question 6 of 7Where should error boundaries go?
Choose an answer to see the explanation.
Question 7 of 7When does React call the root's
onUncaughtError?Choose an answer to see the explanation.
Key takeaways
- Without an error boundary, an error while rendering removes the whole app from the screen.
- A boundary catches errors from rendering, Effects,
use, anduseTransitionactions below it, and shows its fallback instead. - It doesn't catch errors in event handlers, timers, promise callbacks, itself, or server rendering. Hand a handler's error over through state, or use a transition.
- A boundary is a class:
getDerivedStateFromErrorreturns the new state while rendering, andcomponentDidCatchreports after the commit. - To try again, reset its state or give it a new key, after fixing the cause.
- Put boundaries around parts that make sense on their own, plus one near the top.
- The root's
onCaughtError,onUncaughtError, andonRecoverableErrorreport every error in one place.
An error boundary is a fuse for part of the page.
When a component inside breaks while rendering, only that part goes dark, and the rest keeps working.
Coming next: Data fetching libraries. It shows what libraries like TanStack Query and SWR add on top of the cache you used with use: caching, fresh data, retries, and mutations.