Error boundaries in React
Error boundaries are React class components that catch JavaScript errors in their child component tree during rendering, in lifecycle methods, and in constructors, then show a fallback UI while logging the error.
Theory
TL;DR
- Think of it like a circuit breaker in a power grid: one short-circuit trips the breaker, but the rest of the grid stays on.
- Without boundaries, one bad component unmounts the entire React tree. With them, only that subtree fails.
getDerivedStateFromErrorupdates state to trigger the fallback (sync, during render).componentDidCatchlogs the error (post-render, for side effects).- Boundaries only catch render-phase errors in descendants. Event handlers and async code need
try/catch. - Wrap any subtree that can fail independently: third-party widgets, dashboard panels, lazy routes.
Quick example
class ErrorBoundary extends React.Component {
state = { hasError: false };
// Runs during render - returns state update for fallback
static getDerivedStateFromError(error) {
return { hasError: true };
}
// Runs after commit - good place for Sentry or console logging
componentDidCatch(error, info) {
console.error('Caught:', error, info.componentStack);
}
render() {
if (this.state.hasError) return <h1>Something broke!</h1>;
return this.props.children;
}
}
// Wrap the component that might throw
<ErrorBoundary>
<BuggyCounter /> {/* throws during render */}
</ErrorBoundary>
// Result: "Something broke!" instead of a blank screengetDerivedStateFromError fires during the render pass so the fallback appears immediately. componentDidCatch fires after React commits the fallback to the DOM.
Key difference
Before React 16, a rendering error in any component left the DOM in a broken state with no clean recovery path. React 16 introduced fiber architecture with explicit boundary support: when a component throws during render, React walks up the fiber tree looking for the nearest class component with getDerivedStateFromError. That component re-renders with the fallback. Sibling subtrees are untouched. This is pure React fiber traversal, no browser APIs involved.
When to use
- Third-party widgets or embeds: wrap each one independently so a vendor script failure does not take down your whole page.
- Dashboard panels: if one chart's API returns garbage during render, the other panels keep working.
- Lazy-loaded routes: a failed dynamic import won't blank the entire app.
- App-level fallback: one global boundary as a last resort, showing a "reload" button and logging to Sentry.
- Not here: event handlers,
setTimeout,fetchcalls. Usetry/catchor.catch()for those.
How React processes errors internally
React's reconciler tracks errors during the render phase. When a component throws, the error bubbles up through fiber nodes until it hits a class component that has getDerivedStateFromError. That method is static and synchronous, so React gets the new state immediately and re-renders the fallback in the same pass. After the fallback is committed to the DOM, componentDidCatch runs for side effects like logging or sending data to Sentry.
One thing I always remind teammates: if the boundary's own render throws, that error is not caught by itself. It bubbles to the next boundary up the tree. Keep fallback render logic as simple as possible.
Common mistakes
1. Expecting event handler errors to be caught
// NOT caught - event handlers run outside React's render cycle
<ErrorBoundary>
<button onClick={() => { throw new Error('boom'); }}>Click</button>
</ErrorBoundary>Use try/catch inside the handler itself.
2. Skipping getDerivedStateFromError and only using componentDidCatch
// Wrong - setState here queues another render cycle, fallback shows late
componentDidCatch(error, info) {
this.setState({ hasError: true });
}Add static getDerivedStateFromError so the fallback renders in the same pass as the error.
3. Writing a boundary as a functional component
// Won't work
function BadBoundary({ children }) {
const [hasError, setHasError] = useState(false);
// no lifecycle to intercept render errors
}Only class components can be error boundaries. Use the react-error-boundary library if you want a functional-component API.
4. Complex logic inside the boundary's own render
If render in the boundary itself throws, that error is not caught at this level. It goes to the next boundary up. Keep fallback JSX static: no data fetching, no conditions beyond the hasError flag.
Real-world usage
- Create React App: wraps root
<App>in anErrorBoundaryby default since React 16.13. - Next.js:
_error.js(pages router) handles app-level client errors; the app router useserror.jsper route segment. - Sentry: ships
<Sentry.ErrorBoundary>with a built-infallbackprop and automatic error capture. - Redux Toolkit: teams wrap lazy-loaded feature slices in individual boundaries.
- Storybook: per-story boundaries prevent one broken story from killing the whole dev environment.
Follow-up questions
Q: Which phases do error boundaries catch errors in?
A: Render phase, lifecycle methods, and constructors of descendant components. Not event handlers, async code (setTimeout, Promise), SSR, or the boundary's own render method.
Q: What is the difference between getDerivedStateFromError and componentDidCatch?
A: getDerivedStateFromError is static and synchronous. It runs during the render pass and returns new state so the fallback appears immediately. componentDidCatch runs after the commit phase and is for side effects: logging, sending to Sentry, updating metrics.
Q: How do you reset an error boundary after it fires?
A: Call setState({ hasError: false }) from a retry button inside the fallback UI. That causes the boundary to re-render its children from scratch. Some teams also watch for prop changes in componentDidUpdate to auto-reset.
Q: Is there a hook-based equivalent?
A: No official hook exists. Use react-error-boundary, which wraps a class boundary and exposes a clean functional API, including useErrorBoundary for manually triggering boundaries from async code.
Q: In concurrent React 18, how do transitions interact with error boundaries?
A: Errors inside startTransition unwind to the nearest boundary without disrupting the committed UI. React can retry the transition separately. The fiber unwinding mechanism means no "zombie" children remain attached to the tree. Worth mentioning this in senior interviews.
Examples
Basic: catching a render error
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error(error, info.componentStack);
}
render() {
if (this.state.hasError) return <p>Failed to load this section.</p>;
return this.props.children;
}
}
function BuggyComponent() {
throw new Error('render crash'); // throws on every render
}
<div>
<ErrorBoundary>
<BuggyComponent /> {/* shows fallback */}
</ErrorBoundary>
<p>This paragraph still renders fine.</p>
</div>The fallback shows inside the boundary. The sibling paragraph is unaffected because boundaries isolate failures to their own subtree.
Intermediate: production dashboard with retry and Sentry
class ChartPanelBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
Sentry.captureException(error, { extra: errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div className="error-panel">
<h3>Chart failed to load</h3>
<button
onClick={() => this.setState({ hasError: false, error: null })}
>
Retry
</button>
</div>
);
}
return this.props.children;
}
}
function Dashboard() {
return (
<div>
<ChartPanelBoundary>
<RevenueChart dataUrl="/api/revenue" />
</ChartPanelBoundary>
<ChartPanelBoundary>
<UsersChart dataUrl="/api/users" />
</ChartPanelBoundary>
</div>
);
}
// If RevenueChart crashes, UsersChart keeps working
// Retry button sets hasError: false and re-renders children from scratchEach panel gets its own boundary. The retry button calls setState to clear hasError, which causes the boundary to re-render its children fresh.
Advanced: what boundaries do not catch
// 1. Async error - NOT caught by any boundary
function AsyncProblem() {
useEffect(() => {
setTimeout(() => {
throw new Error('async boom'); // boundary misses this
}, 1000);
}, []);
return <p>Loaded</p>;
}
// 2. Event handler error - also NOT caught
function ClickProblem() {
return (
<button onClick={() => { throw new Error('click boom'); }}>
Click me
</button>
);
}
// Correct approach for async errors
function AsyncFixed() {
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.catch(err => setError(err.message)); // handle manually
}, []);
if (error) return <p>Error: {error}</p>;
return <p>Loaded</p>;
}Wrapping these in <ErrorBoundary> does nothing for async or event handler throws. Those must be handled with try/catch or .catch() at the call site.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.