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>What they catch — and what they don’t
Section titled “What they catch — and what they don’t”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
onClickis just a normal JS error; usetry/catch. - Asynchronous code — a rejected promise or a
setTimeoutcallback 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"]
Pairing with Suspense
Section titled “Pairing with Suspense”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.