React · Performance & patterns
Error boundaries
⚡ TL;DR — An error boundary is a class component with
getDerivedStateFromError/componentDidCatchthat catches errors thrown during render/commit of its children and shows a fallback instead of unmounting the whole tree — but it catches nothing from event handlers, async code, or the server.
🧠 Mental model
Without a boundary, a thrown error during rendering unmounts the entire React tree — React 16+ deliberately bails to a blank screen rather than leave a corrupted UI on screen. An error boundary is a circuit breaker: it wraps a subtree, and if anything inside throws while rendering, it isolates the damage to that subtree and renders a fallback while the rest of the app keeps working.
The crucial limitation lives in when React is in control. Boundaries only catch errors thrown inside React's render/reconciliation/commit and lifecycle — the synchronous call stack React itself drives. Anything React isn't on the stack for — a click handler, a setTimeout, a fetch .then — throws to the normal browser, and the boundary never sees it.
⚙️ How it actually works
There is no hook for this. As of React 19 you still write a class (or use react-error-boundary), because the two methods have no hook equivalent:
static getDerivedStateFromError(error)— runs in the render phase; returns new state to switch to the fallback UI. Must be pure (no side effects).componentDidCatch(error, info)— runs in the commit phase; the place to log to Sentry/Datadog.info.componentStacktells you where it threw.
A boundary catches errors from its descendants, never from itself. Granularity matters: one boundary at the root turns any error into a full-page fallback; boundaries around each widget/route isolate failures so one broken chart doesn't blank the dashboard.
What it does NOT catch (interviewers always ask):
- Event handlers → use
try/catch. - Asynchronous code (
setTimeout, promises,fetch) →try/catch+ set error state. - Server-side rendering errors (different code path).
- Errors thrown in the boundary itself.
Uncaught errors during render remount the tree — recovery means giving users a way to reset (a "Try again" that re-mounts the subtree by changing a key or resetting boundary state).
💻 Code
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error }; // render phase: flip to fallback. Pure.
}
componentDidCatch(error, info) {
logToSentry(error, info.componentStack); // commit phase: side effects OK
}
render() {
if (this.state.error) {
return this.props.fallback(this.state.error, () =>
this.setState({ error: null }) // reset → re-mount children, retry
);
}
return this.props.children;
}
}
// ❌ This throw is NOT caught by any boundary — it's an event handler
<button onClick={() => { throw new Error('boom'); }} />;
// ✅ Handle imperative/async errors yourself, then surface via state
async function onSave() {
try {
await api.save();
} catch (e) {
setError(e); // now render a fallback / toast from state
}
}
Pair boundaries with Suspense: Suspense handles the loading state, the boundary handles the failed state — together they cover the full async lifecycle.
⚖️ Trade-offs
- Granularity is a design decision. Root-only = simple but any error blanks everything. Per-route/per-widget = resilient but more boilerplate. Put boundaries at boundaries of independent UI.
- Don't swallow errors silently. A boundary that shows a generic "something went wrong" without logging destroys your ability to debug production. Always report in
componentDidCatch. - When NOT to lean on them: for expected, recoverable failures (a failed API call, a validation error), model them as state, not exceptions — boundaries are for the unexpected.
- Use
react-error-boundaryrather than re-writing the class each time; it addsonReset,resetKeys, and auseErrorBoundaryhook to bridge async errors into a boundary.
💣 Gotchas interviewers probe
- "Do boundaries catch event-handler errors?" No — the single most common trap. Event handlers, async callbacks, and SSR are all outside a boundary's reach.
- Still a class in React 19. There's no hook; be ready to explain why (
getDerivedStateFromError/componentDidCatchhave no hook form yet). getDerivedStateFromErrormust be pure — it runs during render. Logging belongs incomponentDidCatch.- Recovery needs a reset. After catching, the subtree stays on the fallback; you must reset state or change a
keyto retry. - A boundary can't catch its own errors — if the fallback UI throws, it propagates to the next boundary up.
- Bridging async errors: a common pattern is to
catchan async error and thensetStateto something that re-throws during render, so a boundary can handle it uniformly.
🎯 Say this in the interview
"An error boundary is a circuit breaker: a class component with
getDerivedStateFromErrorandcomponentDidCatchthat catches errors thrown while React renders or commits its children, and shows a fallback instead of letting one thrown error unmount the whole tree to a blank screen. The key limitation is scope — it only catches what happens on React's own call stack, so event handlers,setTimeout, promises, and SSR are not covered; those I wrap intry/catchand surface through state. It's still a class in React 19 because those two methods have no hook equivalent, though I'd usually reach forreact-error-boundary. In practice I place boundaries around independent units — routes, widgets — so a broken chart doesn't blank the dashboard, I always log thecomponentStackto Sentry incomponentDidCatch, and I pair them withSuspenseso loading and error states are both covered."
🔗 Go deeper
- react.dev — Catching rendering errors with an error boundary — the class API and exactly what it catches.
- react.dev —
getDerivedStateFromError— render-phase fallback, and why it must be pure. - react-error-boundary — the community-standard wrapper with reset keys and async bridging.
- react.dev —
Suspense— pairing loading and error handling for async UI.