Error Boundaries
Without Error Boundaries, One Bug Crashes Everything
Picture this: you have a beautiful app with 50 components, and one of them throws a TypeError. What happens? A single unhandled error in any component unmounts the entire React tree. The user sees a blank white screen. No error message, no recovery path, nothing.
function BuggyComponent() {
const data = null;
return <div>{data.name}</div>; // TypeError: Cannot read property 'name' of null
}
function App() {
return (
<div>
<Header />
<BuggyComponent /> {/* This error crashes Header and Footer too */}
<Footer />
</div>
);
}
// Result: blank screen. All of App is gone.
Error boundaries prevent this. They catch errors in their child tree and render a fallback UI instead of unmounting everything.
Think of error boundaries as circuit breakers in an electrical system. When a short circuit (error) occurs in one circuit (component subtree), the circuit breaker trips and isolates the failure. The rest of the building (application) keeps running. Without circuit breakers, one failure brings down the entire grid.
Error Boundaries Are Class Components
Yes, you read that right — class components. There is no hooks equivalent for error boundaries. They must be class components that implement either (or both) of two lifecycle methods:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
// Called during render phase — update state to show fallback
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// Called during commit phase — log the error
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error);
console.error('Component stack:', errorInfo.componentStack);
// Send to error tracking service (Sentry, etc.)
reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <DefaultErrorUI error={this.state.error} />;
}
return this.props.children;
}
}
Two Lifecycle Methods, Two Phases
| Method | Phase | Purpose |
|---|---|---|
getDerivedStateFromError | Render | Return new state to trigger fallback UI |
componentDidCatch | Commit | Side effects: logging, error reporting |
getDerivedStateFromError is static and pure — no side effects. It runs during the render phase. componentDidCatch runs during the commit phase and is where you send errors to Sentry, LogRocket, or your logging service.
Why no hooks API for error boundaries
Error boundaries need to catch errors thrown during rendering — the render phase. Hooks like useEffect run after rendering. There is no hook equivalent for intercepting errors thrown during the render phase of child components. The React team has discussed useErrorBoundary but has not shipped it. For now, class components are required. Libraries like react-error-boundary provide a convenient wrapper.
What Error Boundaries Catch (and Don't)
Catches
- Errors thrown during rendering
- Errors in lifecycle methods
- Errors in constructors of child components
Does NOT Catch
- Event handler errors (use try/catch)
- Asynchronous code (setTimeout, promises)
- Server-side rendering errors
- Errors thrown in the boundary itself
function ClickHandler() {
function handleClick() {
// Error boundary will NOT catch this:
throw new Error('Click failed');
}
return <button onClick={handleClick}>Click</button>;
}
// For event handlers, use try/catch:
function SafeClickHandler() {
const [error, setError] = useState(null);
function handleClick() {
try {
riskyOperation();
} catch (err) {
setError(err);
}
}
if (error) return <p>Something went wrong: {error.message}</p>;
return <button onClick={handleClick}>Click</button>;
}
Error boundaries catch errors thrown during React's render phase — not errors in event handlers, async code, or the boundary component itself. If you need to catch errors in event handlers or async operations, use standard try/catch and set error state. The boundary is specifically for catching render-time failures in child components.
Placement Strategy: Granular Boundaries
The thing that separates production apps from tutorials is this: do not wrap the entire app in one boundary. Use multiple boundaries at different granularities:
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
{/* App-level: catches catastrophic errors */}
<Header />
<main>
<ErrorBoundary fallback={<SidebarError />}>
{/* Section-level: sidebar failure does not break content */}
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
{/* Section-level: content failure does not break sidebar */}
<Content />
</ErrorBoundary>
</main>
<Footer />
</ErrorBoundary>
);
}
Error Recovery
Allow users to recover from errors by resetting the boundary state:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
reportError(error, info);
}
resetError = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.resetError}>Try Again</button>
</div>
);
}
return this.props.children;
}
}
Resetting on Navigation
Reset the boundary when the user navigates to a different page:
class ErrorBoundary extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.resetKey !== this.props.resetKey) {
this.setState({ hasError: false });
}
}
// ... rest of the boundary
}
// Usage with React Router:
function App() {
const location = useLocation();
return (
<ErrorBoundary resetKey={location.pathname}>
<Routes>{/* ... */}</Routes>
</ErrorBoundary>
);
}
Production Scenario: The react-error-boundary Library
Most production apps use the react-error-boundary library for a cleaner API:
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => reportError(error, info)}
onReset={() => {
// Reset app state that caused the error
}}
>
<Dashboard />
</ErrorBoundary>
);
}
-
Wrong: Wrapping the entire app in one error boundary Right: Use multiple boundaries at different granularities — page, section, component
-
Wrong: Expecting error boundaries to catch event handler errors Right: Use try/catch in event handlers and set error state
-
Wrong: Not providing error recovery (retry/reset) Right: Include a reset mechanism — button, navigation change, or time-based retry
-
Wrong: Trying to use hooks to create an error boundary Right: Use a class component or the react-error-boundary library
- 1Error boundaries catch render-phase errors in children — not event handlers or async code
- 2Class components only — no hooks API exists for error boundaries
- 3getDerivedStateFromError (render phase) for fallback UI, componentDidCatch (commit phase) for logging
- 4Place boundaries at multiple granularities — app level, page level, section level
- 5Always provide error recovery — retry button, navigation reset, or automatic retry