flowchart TD
subgraph WithBoundary["With Error Boundary ✅"]
A1["App"] --> B1["ErrorBoundary"]
B1 --> C1["Widget"]
C1 -->|"💥 Crash"| D1["Fallback UI"]
B1 --> E1["Rest of App ✓"]
end
subgraph WithoutBoundary["Without Error Boundary ❌"]
A2["App"] --> C2["Widget"]
C2 -->|"💥 Crash"| D2["Entire App Crashes"]
end
style D1 fill:#fbbf24
style D2 fill:#ef4444
style E1 fill:#10b981
Error Boundaries: Graceful Failure in React
As your application grows, more components are added and it becomes harder to track all possible exceptions during rendering. React offers Error Boundaries — special components that catch errors and display fallback UI instead of crashing your entire app.
What Does an Error Boundary Catch?
| Caught ✅ | Not Caught ❌ |
|---|---|
| Errors during rendering | Event handlers |
| Lifecycle method errors | Async code (setTimeout, fetch) |
| Constructor errors | Server-side rendering |
| Errors in child components | Errors in the boundary itself |
Error boundaries only catch errors during the render phase. For event handlers, you still need traditional try-catch blocks.
Where to Place Error Boundaries
This is the most common question. Here’s the strategic placement pattern:
flowchart TD
App["🏠 App"]
App --> Layout["Layout"]
Layout --> Sidebar["📁 Sidebar"]
Layout --> Main["📄 Main Content"]
Main --> Route1["Route: Dashboard"]
Main --> Route2["Route: Settings"]
Route1 --> Widget1["📊 Chart Widget"]
Route1 --> Widget2["📈 Analytics Widget"]
Route1 --> Widget3["🔔 Notifications"]
subgraph Boundaries["Error Boundary Placement"]
B1["🛡️ Route-Level<br/>(catches page crashes)"]
B2["🛡️ Widget-Level<br/>(isolates failures)"]
B3["🛡️ Third-Party<br/>(untrusted code)"]
end
style App fill:#3b82f6
style B1 fill:#fbbf24
style B2 fill:#fbbf24
style B3 fill:#fbbf24
Placement Strategy
| Level | When to Use | Example |
|---|---|---|
| App Root | Last resort fallback | “Something went wrong. Reload?” |
| Route/Page | Each route gets its own boundary | Dashboard, Settings, Profile |
| Feature/Widget | Independent features that can fail | Charts, Comments, Chat |
| Third-Party | External libraries you don’t control | Embeds, Plugins, SDKs |
The Modern Way: react-error-boundary
The react-error-boundary library provides a clean, hook-friendly API:
npm install react-error-boundaryBasic Usage
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" className="error-fallback">
<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) => {
// Log to Sentry, LogRocket, etc.
console.error('Error caught:', error, info);
}}
onReset={() => {
// Reset app state if needed
window.location.reload();
}}
>
<Dashboard />
</ErrorBoundary>
);
}
useErrorBoundary Hook
For programmatic error throwing:
import { useErrorBoundary } from 'react-error-boundary';
function DataFetcher() {
const { showBoundary } = useErrorBoundary();
const fetchData = async () => {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
} catch (error) {
showBoundary(error); // Trigger the error boundary
}
};
// ...
}
Component Hierarchy Example
Here’s a real-world component tree with strategic error boundary placement:
// App.tsx
import { ErrorBoundary } from 'react-error-boundary';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
{/* 🛡️ Level 1: App-wide crash protection */}
<ErrorBoundary FallbackComponent={AppCrashFallback}>
<Layout>
<Sidebar />
<main>
{/* 🛡️ Level 2: Route-level isolation */}
<ErrorBoundary FallbackComponent={PageErrorFallback}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</ErrorBoundary>
</main>
</Layout>
</ErrorBoundary>
</BrowserRouter>
);
}
// Dashboard.tsx
function Dashboard() {
return (
<div className="dashboard">
<h1>Dashboard</h1>
<div className="widgets">
{/* 🛡️ Level 3: Widget-level isolation */}
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<AnalyticsChart />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<RecentActivity />
</ErrorBoundary>
{/* 🛡️ Level 4: Third-party isolation */}
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<ThirdPartyEmbed />
</ErrorBoundary>
</div>
</div>
);
}
Fallback UI Patterns
Minimal Widget Fallback
function WidgetErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="widget-error">
<span>⚠️ Failed to load</span>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}
Informative Page Fallback
function PageErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="page-error">
<h2>This page encountered an error</h2>
<p>We've been notified and are working on a fix.</p>
<details>
<summary>Error details</summary>
<pre>{error.message}</pre>
<pre>{error.stack}</pre>
</details>
<div className="actions">
<button onClick={resetErrorBoundary}>Try again</button>
<button onClick={() => window.location.href = '/'}>
Go to homepage
</button>
</div>
</div>
);
}
Full App Crash Fallback
function AppCrashFallback({ error }) {
return (
<div className="app-crash">
<h1>😵 Something went wrong</h1>
<p>The application has crashed. Please reload the page.</p>
<button onClick={() => window.location.reload()}>
Reload Application
</button>
</div>
);
}
Production Error Logging
Integrate with error tracking services:
import * as Sentry from '@sentry/react';
import { ErrorBoundary } from 'react-error-boundary';
function logError(error: Error, info: { componentStack: string }) {
Sentry.captureException(error, {
extra: {
componentStack: info.componentStack,
},
});
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={logError}
>
<Routes />
</ErrorBoundary>
);
}
The Manual Way: Class Component
If you need zero dependencies or want to understand how it works:
import React, { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.props.onError?.(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div>Something went wrong.</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Best Practices
- Don’t wrap everything — Be strategic, not paranoid
- Isolate untrusted code — Third-party libraries get their own boundary
- Reset on navigation — Use
resetKeyswith route changes - Log to a service — Sentry, LogRocket, or your own backend
- Keep fallbacks helpful — Show actionable UI, not just “Error”
- Test your boundaries — Intentionally throw errors in development
| Do ✅ | Don’t ❌ |
|---|---|
| Wrap routes independently | Wrap entire app in one boundary |
| Isolate widgets | Let one widget crash the page |
| Log errors to a service | Silently swallow errors |
| Provide retry buttons | Show only “Something went wrong” |
| Reset on navigation | Keep error state forever |
Testing Error Boundaries
// ErrorBoundary.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ErrorBoundary } from 'react-error-boundary';
function ThrowError() {
throw new Error('Test error');
}
test('renders fallback when child throws', () => {
render(
<ErrorBoundary fallback={<div>Error occurred</div>}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Error occurred')).toBeInTheDocument();
});
test('resets when resetErrorBoundary is called', async () => {
const user = userEvent.setup();
let shouldThrow = true;
function MaybeThrow() {
if (shouldThrow) throw new Error('Test error');
return <div>Success</div>;
}
render(
<ErrorBoundary
FallbackComponent={({ resetErrorBoundary }) => (
<button onClick={resetErrorBoundary}>Reset</button>
)}
onReset={() => { shouldThrow = false; }}
>
<MaybeThrow />
</ErrorBoundary>
);
await user.click(screen.getByText('Reset'));
expect(screen.getByText('Success')).toBeInTheDocument();
});
Error boundaries transform your app from “one crash kills everything” to “graceful degradation with user feedback.” Place them strategically, log errors properly, and always give users a way to recover.