AniUI Academy

Error Boundaries

Catching a render error in a subtree before it takes down the whole app — why this specific job still requires a class component, and what error boundaries deliberately don't catch.

8 min read

Every component you've written assumes rendering succeeds. Real components sometimes throw — a malformed API response, an unexpected undefined, a third-party library misbehaving. Error boundaries are React's mechanism for containing that damage to a section of the UI instead of losing the whole page.

What happens without one

By default, an error thrown during rendering anywhere in the tree is serious enough that React unmounts the affected tree rather than risk leaving a half-rendered, inconsistent UI on screen. With no boundary anywhere above the failure, that can mean the entire app goes blank.

function UserBio({ user }) {
  return <p>{user.bio.toUpperCase()}</p>; // throws if user.bio is undefined
}

One malformed user record, with no boundary in place, can take down everything — the header, the sidebar, unrelated panels that had nothing to do with the bad data.

Defining an error boundary

This is one of the few corners of modern React that still genuinely requires a class component — there is no hook-based equivalent as of React 18 or 19:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
 
  componentDidCatch(error, info) {
    logErrorToService(error, info);
  }
 
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

getDerivedStateFromError runs during rendering, to decide what the fallback UI should be; componentDidCatch runs afterward, as the place to log or report the error. Wrapping part of the tree in it:

<ErrorBoundary fallback={<p>Something went wrong loading this section.</p>}>
  <UserBio user={user} />
</ErrorBoundary>

If UserBio throws, only this section shows the fallback message — the rest of the page, outside the boundary, keeps working normally. In practice, most real projects don't hand-write this class themselves; they use a small, well-tested library like react-error-boundary, which wraps the same mechanism in a reusable, more ergonomic component while still relying on this exact class-based API underneath.

What it deliberately does not catch

Error boundaries only catch errors from the specific process of building the UI: render, lifecycle methods, and constructors of components below them. They do not catch errors from:

  • Event handlers — a throw inside onClick needs an ordinary try/catch, since it runs later, outside of rendering.
  • Asynchronous code — an error inside a setTimeout callback or an unhandled promise rejection inside an effect, for the same reason.
  • Errors in the boundary component itself — a boundary can't catch its own failures; that's why boundaries are usually kept intentionally simple.
function DeleteButton() {
  function handleClick() {
    try {
      deleteItem();
    } catch (error) {
      // handled here, not by any error boundary above this
      showToast("Failed to delete");
    }
  }
  return <button onClick={handleClick}>Delete</button>;
}

This split isn't a gap in the design — event handlers and effects run in response to something that already happened, outside the render process a boundary is watching, so ordinary JavaScript error handling is the right tool there.

Placement is a deliberate choice

A single boundary around the entire app turns any rendering error anywhere into "the whole app shows one fallback message" — better than a blank page, but coarse. Scoping boundaries around independent sections (a chat widget, a comments panel, a third-party embed) means one broken feature degrades gracefully while everything else keeps functioning — usually the more useful granularity for a real application.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • Without an error boundary, a render error can unmount the whole tree rather than leave a broken UI on screen — boundaries contain that damage to a subtree.
  • Error boundaries still require a class component (getDerivedStateFromError / componentDidCatch) — one of the few places without a hook equivalent.
  • They only catch errors from rendering, lifecycle methods, and constructors — not event handlers or async code, which still need ordinary try/catch.
  • Scope boundaries around independent sections of the UI, not just the whole app, so one broken feature doesn't take everything else down with it.

Check yourself

4 questions · pass 3/4 to unlock Portals

up to 50
  1. 1.What happens by default if a component throws an error during rendering, with no error boundary anywhere above it?

  2. 2.As of React 18/19, how do you define an error boundary?

  3. 3.A button's onClick handler throws an error. Does the error boundary wrapping that button catch it?

  4. 4.What should an error boundary's fallback UI typically do?

4 left to answer