Skip to Content
React in a Week
Day 5 · Scaling up (TypeScript)

Error boundaries

Stop one broken component from taking down the whole page, and show a helpful fallback instead.

Bugs happen. Data arrives in a shape you did not expect, a value you assumed exists turns out to be missing, a library has an edge case. When that happens while React is rendering, the result is harsh: by default React removes the entire app from the screen. One broken book card means a blank page.

An error boundary is a component that catches errors thrown by components inside it and shows a fallback UI instead, while the rest of the app keeps working. Think of it as a try/catch for a part of your component tree.

When rendering fails

Click "Show covers" below. One book has bad data, and the cover component throws.

Live code· One error, whole app gone

The preview shows an error instead of your app. The heading, the button and the unrelated paragraph are all gone. React does this on purpose: rather than leave a half-broken, possibly misleading UI on screen, it unmounts everything. Click the refresh icon on the preview to start over.

The fix for the bug is to handle missing colors. But you cannot foresee every bug, so you also want a safety net that limits the damage.

Catching errors with react-error-boundary

React's own docs are blunt: there is currently no way to write an error boundary as a function component. It has to be a class component, which this course otherwise never uses. You do not need to write that class yourself, though. The small, widely used react-error-boundary package provides a ready-made ErrorBoundary, and react.dev recommends it. Install it with npm install react-error-boundary.

Live code· A boundary around the covers

Now only the part inside the boundary is replaced. The heading, the button and the paragraph survive. Notice that the whole boundary's content is replaced, including The Hobbit's cover, which was fine. A boundary swaps out everything inside it, so where you put it decides how much disappears.

In development, React still logs caught errors with console.error, and some sandboxes show that log. That does not mean the boundary failed. If the fallback is on screen, it worked. In production builds the log is the only trace, which is why you usually report errors to a logging service (see onError below).

Showing details and letting users recover

A fixed message is a start. Usually you want to show what went wrong and offer a way out. Instead of fallback, pass a FallbackComponent. It receives two props:

  • error: whatever was thrown. Its type is unknown, because JavaScript lets you throw anything, so you check error instanceof Error before reading .message.
  • resetErrorBoundary: a function that clears the error and renders the children again.

The boundary can also reset automatically when something changes. Pass resetKeys={[selectedId]}, and when selectedId changes while an error is showing, the boundary resets and tries again.

In the sandbox, the third book has no rating and crashes the details panel. Pick it, then pick another book.

Live code· Fallback component and resetKeys

Four props did the work:

  • FallbackComponent renders the friendly panel with the real message.
  • resetKeys={[selectedId]} resets the boundary when you pick a different book, so the user is never stuck on the error.
  • onReset runs on every reset. Its details.reason is 'keys' for a resetKeys change and 'imperative-api' when resetErrorBoundary was called, so the button can move back to a book that works.
  • onError is called once per caught error. In a real app you send the error to a service like Sentry here.

What boundaries do not catch

Error boundaries catch errors thrown while React renders components inside them. They do not catch errors thrown:

  • inside event handlers, like onClick;
  • in asynchronous code, like a setTimeout callback or after an await;
  • in the error boundary component itself.

That is because those errors do not happen during rendering. React is not on the call stack, so it cannot catch them. For event handlers, a normal try/catch is often enough. When you want such an error to show the nearest boundary's fallback, react-error-boundary gives you the useErrorBoundary hook. Its showBoundary(error) function hands the error to the nearest boundary.

Live code· Async errors with showBoundary

This one fails about half the time, so click "Load reviews" a few times. Here "Try again" makes sense, because the failure is random, like a flaky network. fallbackRender is a third way to give a fallback: a function that receives the same props as FallbackComponent.

useErrorBoundary must be called in a component inside an ErrorBoundary, or it throws. Data libraries can also do this for you: TanStack Query's useQuery accepts throwOnError: true, which sends failed queries to the nearest boundary.

Under the hood: the class version

For the curious, this is roughly what an error boundary looks like if you write it yourself. It is the only class component in this course, and you do not need to memorize it.

ErrorBoundary.tsx
import { Component } from 'react';
import type { ErrorInfo, ReactNode } from 'react';

interface Props {
  fallback: ReactNode;
  children: ReactNode;
}

interface State {
  hasError: boolean;
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false };

  // Runs during rendering when a child throws: switch to the fallback.
  static getDerivedStateFromError(): State {
    return { hasError: true };
  }

  // Runs after the error is caught: a good place to log it.
  componentDidCatch(error: unknown, info: ErrorInfo) {
    console.error(error, info.componentStack);
  }

  render() {
    return this.state.hasError ? this.props.fallback : this.props.children;
  }
}

The two special methods, static getDerivedStateFromError and componentDidCatch, have no hook equivalents yet. That is the whole reason error boundaries are still classes. react-error-boundary is this same idea plus resetting, fallback components and the showBoundary hook.

Deep diveWhere to put boundaries

There is no single right answer, but a useful rule of thumb is: one boundary per thing that can fail independently.

  • One top-level boundary around the whole app, so the user always sees "Something went wrong, reload" instead of a blank page.
  • One per page or route, so a broken detail page leaves the navigation working. You will add exactly this to the Reading List in the checkpoint, with resetKeys={[id]} so moving to another book clears the error.
  • One per independent widget, like a sidebar or a reviews panel, when the rest of the page is still useful without it.

Wrapping every small component is too much: users end up with a page full of tiny error messages. React 19 also lets you pass onCaughtError and onUncaughtError options to createRoot, which is a central place to report errors from every boundary at once.

Try it

Keep the dashboard alive

This dashboard has three widgets. The Streak widget crashes. Right now that blanks the entire dashboard. Add error boundaries so that only the broken widget shows a small "This widget failed to load" message, and the other two keep working.

Live code· Challenge: widget boundaries
Show solution
App.tsx
import type { ReactNode } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { CurrentlyReading, Goal, Streak } from './widgets';

function Widget({ children }: { children: ReactNode }) {
  return (
    <ErrorBoundary fallback={<section className="card failed">This widget failed to load.</section>}>
      {children}
    </ErrorBoundary>
  );
}

export default function App() {
  return (
    <div className="grid">
      <Widget>
        <CurrentlyReading />
      </Widget>
      <Widget>
        <Streak />
      </Widget>
      <Widget>
        <Goal />
      </Widget>
    </div>
  );
}

Each widget gets its own boundary, so a failure is contained to one card. Wrapping the boundary in a small Widget component keeps App readable, and gives you a single place to add logging later with onError. One boundary around the whole grid would have replaced all three widgets with a single message.

Quick check

A button's onClick handler throws an error. The button is inside an ErrorBoundary. What happens?

Quick check

Why is an error boundary still a class component in React 19?

Day 5 progress

0/7
Next: Day 6: Modern React 19

On this page