Learn how to gracefully handle runtime errors in React using error boundaries — class components that catch JavaScript errors in their child component tree and display a fallback UI.
In a normal JavaScript application, a single unhandled error can crash the entire page. React applications face the same problem — if a component throws an error during rendering, the entire component tree unmounts, leaving the user with a blank screen.
Error boundaries solve this by catching JavaScript errors anywhere in their child component tree, logging those errors, and displaying a fallback UI instead of crashing the whole application.
Think of error boundaries like a try...catch block, but for React components. Just as you wrap risky code in try/catch to prevent your program from crashing, you wrap risky components in an error boundary to prevent your UI from going blank.
Key points about error boundaries:
Error boundaries are one of the few remaining use cases for class components in modern React. As of React 19, there is still no hook equivalent for catching render errors. The React team has discussed adding one, but it has not shipped yet.
A class component becomes an error boundary when it defines one or both of these lifecycle methods:
static getDerivedStateFromError(error)
static getDerivedStateFromError(error) {
// Update state so the next render shows the fallback UI
return { hasError: true };
}componentDidCatch(error, errorInfo)
componentStack property showing which component in the tree threw.componentDidCatch(error, errorInfo) {
// Log the error to an error reporting service
console.error('Error caught:', error);
console.error('Component stack:', errorInfo.componentStack);
}You typically use getDerivedStateFromError to render the fallback UI and componentDidCatch to log the error. Both methods can be used together in the same component.
A well-designed error boundary is reusable across your application. Here is a complete pattern:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback || <h2>Something went wrong.</h2>;
}
return this.props.children;
}
}Usage:
<ErrorBoundary fallback={<p>Oops! This section failed to load.</p>}>
<RiskyComponent />
</ErrorBoundary>Notice the fallback prop — it lets you customize the error message for each usage without modifying the error boundary itself. The this.props.children pattern renders the child components normally when there is no error.
Some libraries, like react-error-boundary, provide a ready-made component with more features like error recovery and reset. But understanding how to build one from scratch is essential because:
Showing a fallback UI is only half the solution — you also want to give users a way to recover from the error. Common strategies include:
1. Retry button:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
handleReset = () => {
this.setState({ hasError: false });
};
render() {
if (this.state.hasError) {
return (
<div>
<p>Something went wrong.</p>
<button onClick={this.handleReset}>Try Again</button>
</div>
);
}
return this.props.children;
}
}When the user clicks "Try Again", the state resets and React attempts to re-render the children. If the error was transient (like a network issue), the re-render may succeed.
2. Key-based reset:
Changing the key prop on an error boundary forces React to unmount and remount it, resetting its state:
<ErrorBoundary key={resetKey}>
<DataComponent />
</ErrorBoundary>3. Navigate away: In a routed application, you can redirect the user to a safe page when an error occurs.
Important: If the underlying bug is not fixed, the component will throw the same error again on re-render. Recovery strategies work best for transient errors (network failures, race conditions) rather than logic bugs.
Where you place error boundaries matters. You should not wrap your entire app in a single error boundary — that is like catching every exception at the top level. Instead, use a layered approach:
1. Top-level boundary — Catch truly unexpected errors and show a full-page error screen:
<ErrorBoundary fallback={<FullPageError />}>
<App />
</ErrorBoundary>2. Route-level boundaries — Isolate errors to individual pages so navigation still works:
<Route path="/dashboard">
<ErrorBoundary fallback={<p>Dashboard failed to load.</p>}>
<Dashboard />
</ErrorBoundary>
</Route>3. Feature-level boundaries — Protect specific widgets so the rest of the page remains functional:
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<ErrorBoundary fallback={<p>Chart unavailable.</p>}>
<AnalyticsChart />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Feed unavailable.</p>}>
<ActivityFeed />
</ErrorBoundary>
</div>
);
}This way, if the analytics chart crashes, the activity feed and the rest of the dashboard remain visible.
Limitations to remember:
.catch() or try/catch in async functions.<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("Caught by ErrorBoundary:", error);
}
render() {
if (this.state.hasError) {
return <div id="error">Something went wrong</div>;
}
return this.props.children;
}
}
function BuggyComponent() {
throw new Error("I crashed!");
return <p>This will never render</p>;
}
function SafeComponent() {
return <p>I am working fine!</p>;
}
function App() {
return (
<div>
<h1>Error Boundary Demo</h1>
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
<SafeComponent />
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>Why must error boundaries be class components in React?