Skip to content

Error Boundaries

A crash in one component shouldn’t blank the whole app

Section titled “A crash in one component shouldn’t blank the whole app”

If a component throws during render, React — by design — unmounts the entire tree rather than show a corrupt UI. An error boundary is a component that catches such errors in its subtree, so a failure in one widget shows a fallback there instead of taking down the whole page.

Error boundaries are still written as class components, because they rely on two lifecycle methods with no hook equivalent:

import { Component } from "react";
class ErrorBoundary extends Component {
state = { error: null };
// Render phase: return new state to show a fallback on the next render.
static getDerivedStateFromError(error) {
return { error };
}
// Commit phase: side effect — log the error to your reporting service.
componentDidCatch(error, info) {
reportError(error, info.componentStack);
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}

In app code most people use the tiny react-error-boundary library, which wraps this and adds a reset mechanism — but the mechanics are exactly the above.

<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<Widget /> {/* if Widget throws while rendering, the fallback shows here */}
</ErrorBoundary>

This is the part people get wrong. Error boundaries catch errors that happen during rendering (and in lifecycle methods and constructors) of the components below them. They do not catch:

  • Event handlers — an error in an onClick is just a normal JS error; use try/catch.
  • Asynchronous code — a rejected promise or a setTimeout callback runs outside React’s render, so no boundary sees it.
  • Errors in the boundary itself — only errors below it.
  • Server-side rendering errors — handled separately.
flowchart TB
  eb["Error Boundary"] --> child["child renders"]
  child -->|throws during render| caught["caught → show fallback"]
  handler["onClick throws"] -.NOT caught.-> handler2["use try/catch"]
  async["promise rejects / setTimeout"] -.NOT caught.-> async2["handle it yourself"]
What an error boundary catches

Suspense and error boundaries are complementary: Suspense handles the loading state (a child is waiting), an error boundary handles the failure state (a child threw — including a rejected promise read by use()). Together they give you the three states of async UI declaratively:

<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Spinner />}>
<Profile /> {/* loading → Spinner, error → Error, success → Profile */}
</Suspense>
</ErrorBoundary>

When use() reads a promise that rejects, React propagates it to the nearest error boundary — so the same boundary that handles render crashes also handles data-fetch failures.

What does an error boundary catch?
Which of these does an error boundary NOT catch?
Why are error boundaries still written as class components?
How do Suspense and error boundaries work together?