Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ~33 min· topic 5 of 7

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

ErrorBoundary.jsxwhole filejsx
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.

Place boundaries around independent partsdiagram
Rendering diagram…

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.

Know where errors godiagram
Rendering diagram…

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.

main.jsxwhole filejsx
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. 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 the screen: line would crash on null; that is exactly the point.)

  2. 2

    Reset with a key instead

    In the third example, remove resetKeys and 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. 3

    Catch the handler error yourself

    In the second example, change SaveButton to keep an error state and wrap the throw in try/catch, showing <p>Save failed</p>. Or rethrow it with the setState(() => { throw e }) trick and watch the boundary take over.

Code & diagrams

A boundary keeps the rest of the page alive New tab

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

Press Run to see the page.

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.
What a boundary does not catch, and the rethrow trick New tab

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

Press Run to see the page.

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 failed
A react-error-boundary style boundary with reset New tab

Picking 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

Press Run to see the page.

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: Laptop
Root error callbacks: caught, recoverable and uncaught React 19+ New tab

Each 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

Press Run to see the page.

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)
The same with the react-error-boundary package (npm install react-error-boundary)jsx
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.

terminal
$ Console
── what you'll see ──
Uncaught Error: click failed

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

terminal
$ Console after clicking Product 3
── what you'll see ──
logged: No product 3
logged: No product 3
click 3: Something went wrong: No product 3

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.log in a throwing component prints twice). If the retry succeeds, the error is reported to onRecoverableError instead.

  • ▸

    Since React 19, errors in useEffect and useLayoutEffect setup 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 with use. With TanStack Query, throwOnError: true makes queries throw into the nearest boundary, and QueryErrorResetBoundary resets failed queries when the boundary resets (Topic 14.6).

Remember this

  1. 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. 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 }, so render() 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.componentStack is a string showing which components the error passed through. You write one boundary class and reuse it everywhere with different fallbacks.

  3. 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 use try/catch there 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. 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. 5

    **react-error-boundary.** Most teams don't hand-write boundaries; they install the small react-error-boundary package. Its <ErrorBoundary> takes fallback, fallbackRender or FallbackComponent (which receives error and resetErrorBoundary), onError for logging, onReset, and resetKeys. It also provides useErrorBoundary(), whose showBoundary(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. 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; and onRecoverableError(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

01

What is an error boundary and how do you write one?

02

What's the difference between getDerivedStateFromError and componentDidCatch?

03

Which errors do error boundaries not catch, and how do you handle those?

04

How do you reset an error boundary? Compare a Try again button, reset keys and the key prop.

05

What are onCaughtError, onUncaughtError and onRecoverableError, and when would you use them?

Practice

01

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.

02

A DeleteButton calls await api.delete(id) in its click handler. Make a failure show in the nearest error boundary.

03

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-boundary already 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-boundary and its showBoundary.

  • I can configure onCaughtError, onUncaughtError and onRecoverableError on a root.