cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

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.

By the end, you can
  • 01
    Know 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.
  • 02
    Write and reset a boundaryWrite an error boundary class with getDerivedStateFromError and componentDidCatch, and let the reader try again with a reset or a new key.
  • 03
    Place boundaries and report errorsPut boundaries around parts that can fail on their own, and report errors with the root's onCaughtError, onUncaughtError, and onRecoverableError.

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:

A product page with one bad reviewPop out in the code editor (opens in a new tab)JSX
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 />    </>  );}
ResultLoading…

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:

The same page, with the reviews inside an error boundaryPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

A function card, App, with arrows to three children: Carrot cake, $4; Add to cart; and ErrorBoundary. Below ErrorBoundary, a faded, crossed-out Reviews card that threw, and a fallback card shown instead. The heading and the button stay on screen.AppCarrot cake, $4Add to cartErrorBoundaryoutside the boundary: they stayReviewsfallbackthrewshown instead
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.
Real-life analogyFuses in a house

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
The main idea

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.

Two columns. Caught by a boundary above: while rendering, in an Effect, use with a rejected promise, and a useTransition action. Not caught: in an event handler, in setTimeout or a then callback, in the boundary itself, and during server rendering.Caught by a boundary abovewhile renderingin an Effectuse(rejected promise)useTransition's actionNot caught by itin an event handlerin setTimeout or .then()in the boundary itselfduring server rendering
A boundary catches errors from React's own work on the components below it: rendering, Effects, 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.

Playground: what does the boundary catch?
The page

Pick a button below.

Who got the error
onCaughtError
not called
onUncaughtError
not called
Try it yourself
Pick a place to throw

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.

The page runs here, with React's real error boundaries, in a root of its own. Its onCaughtError and onUncaughtError options report to the right side instead of the console. Reset clears it.

Read the table row by row: where the error is thrown, whether a boundary above catches it, and why.

Where an error can start
Where the error is thrownCaught by a boundary above?Why
A component's body, while it rendersYes.React is rendering the tree below the boundary, so it knows which boundary is nearest.
An Effect, like useEffectYes.React runs Effects as part of its work on the tree, and knows which component each one belongs to.
use with a rejected promiseYes.use throws the reason while the component renders.
A function passed to useTransition's startTransitionYes.React runs it as part of the component's update and hands its error to the nearest boundary.
An event handler, like onClickNo.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 fallbackNo.A boundary can't catch its own errors. The next boundary up gets them.
Server rendering, when the page is made into HTMLNo.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:

A click handler's error goes past the boundaryPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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:

A click handler's error, handed to the boundaryPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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.)

An order placed in a transitionPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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:

ErrorBoundary.jsxJSX
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 in this.state, reads its props from this.props, and returns what to show from a render method.
  • Line 4 starts the state with no error: { error: null }.
  • Lines 6 to 8: when a component inside throws, React calls getDerivedStateFromError with the error, while it's rendering. It returns the new state, { error }. It's static, so it belongs to the class and can't touch this: it only returns state.
  • Lines 10 to 13: once the fallback is on screen, React calls componentDidCatch with the error and an info object. info.componentStack lists 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: render shows this.props.fallback when there's an error, and the children otherwise.
Four cards from top to bottom, joined by arrows. Reviews throws an error, while rendering. getDerivedStateFromError(error) returns the error as new state. render() returns the fallback, which goes on screen. componentDidCatch(error, info) reports it.Reviews throws an errorwhile renderinggetDerivedStateFromError(error)returns { error }render() returns the fallbackon screencomponentDidCatch(error, info)report it
React calls 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.

Class components, briefly

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:

Try again with a new keyPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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:

ErrorBoundary.jsx with a resetJSX
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:

Trying again while the data is still brokenPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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.

Two ways to try again
QuestionA reset methodA 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 whenThe 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.

A page window holding Header and Cart cards, and three smaller windows, each titled boundary, holding Products, Reviews, and Recommendations. Recommendations is faded and crossed out, with a note that only it couldn't load.pageHeaderCartboundaryProductsboundaryReviewsboundaryRecommendationsonly this one couldn't load
One boundary around each widget. When 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:

Playground: where should the boundary go?
App.jsxJSX
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>
    </>
  );
}
The shop pageworking
Try it yourself
Everything works

Pick where the error boundaries go, then press Break the recommendations. The page starts again whenever you pick.

The shop page runs here, with React's real error boundaries, in a root of its own. The code on the left is its App.jsx for your choice; the widgets are one-line components. Reset puts the boundaries around each widget and mends the recommendations.

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:

A root that reports every errorPop out in the code editor (opens in a new tab)JSX
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>,);
ResultLoading…

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:

An error React recovers fromPop out in the code editor (opens in a new tab)JSX
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>,);
ResultLoading…

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.

The root's error options
OptionReact calls it whenWithout it, React
onCaughtErrorAn error boundary caught an error.Logs the error with console.error, with the component it came from.
onUncaughtErrorNo 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.
onRecoverableErrorReact 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.

Report, don't render

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.)

The coupon with react-error-boundaryJSX
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: useErrorBoundary gives showBoundary. Line 9 hands the handler's error to the nearest boundary with it, the same job the state and throw did in the coupon example.
  • Lines 18 to 20: fallbackRender gets the error and resetErrorBoundary, the package's reset.
  • Line 21: onError plays the part of componentDidCatch, 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.

A product page with a boundary per widgetPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…
  • Lines 38 to 45: the stock has its own boundary, keyed by stockTry on line 39. When the first check fails, line 40 shows Stock unknown. and Check again.
  • Lines 29 to 32: checkAgain drops the failed answer and changes the key, so a fresh boundary asks again and shows Carrot 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.

Does a boundary catch it?
  • throw in a component's body, while it renders
  • throw in an onClick handler
  • throw inside useEffect
  • throw in a setTimeout callback
  • use(promise) with a promise that rejects
  • throw in a .then() callback, after the data arrives
  • throw in a function passed to useTransition's startTransition
  • throw in the error boundary's own render
Try it yourself
0 of 8 correct

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?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "An error boundary catches every error in the components inside it." No. It catches errors from rendering, Effects, use, and useTransition actions. 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 componentDidCatch or the root's onCaughtError.
  • "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.

Exercise 1 · Warm-upWhich option hears about it?

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>,);
ResultLoading…

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upKeep the page when the reviews break

    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 />    </>  );}
    ResultLoading…

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeReport the error

      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>    </>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeMake Try again work

        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>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeSend the coupon's error to the boundary

          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>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeReport every caught error from the root

            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>,);
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              The error boundaries quiz · 7 questionsScore: first tries count
              1. Question 1 of 7You click Apply coupon. What does the page show?

                Read the code, then predictJSX
                function 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.

              2. Question 2 of 7Reviews throws while rendering. What happens to Header?

                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.

              3. Question 3 of 7Which of these errors does an error boundary above the component catch?

                Choose an answer to see the explanation.

              4. Question 4 of 7Why must getDerivedStateFromError only return the new state, while componentDidCatch may send a report?

                Choose an answer to see the explanation.

              5. Question 5 of 7A boundary shows its fallback. How do you show its children again?

                Choose an answer to see the explanation.

              6. Question 6 of 7Where should error boundaries go?

                Choose an answer to see the explanation.

              7. 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, and useTransition actions 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: getDerivedStateFromError returns the new state while rendering, and componentDidCatch reports 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, and onRecoverableError report 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.

              CompleteFrontend Clear concepts. Working examples.