What are Error Boundaries?
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire app.
🎯 Basic Error Boundary
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state so next render shows fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught by boundary:', error, errorInfo);
this.setState({
error: error,
errorInfo: errorInfo
});
// Send to monitoring service (e.g., Sentry)
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>Something went wrong</h2>
<p>We're sorry for the inconvenience.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<Header />
<MainContent />
<Footer />
</ErrorBoundary>
);
}
🔄 Error Boundary with Reset
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
}
resetError = () => {
this.setState({ hasError: false });
};
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Oops! Something went wrong</h2>
<button onClick={this.resetError}>Try Again</button>
</div>
);
}
return this.props.children;
}
}
// Usage with key to force remount
function App() {
const [resetKey, setResetKey] = useState(0);
return (
<ErrorBoundary key={resetKey}>
<ProblematicComponent />
<button onClick={() => setResetKey(k => k + 1)}>
Reset
</button>
</ErrorBoundary>
);
}
📝 Detailed Error Display (Development)
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({ error, errorInfo });
}
render() {
if (this.state.hasError) {
const isDevelopment = process.env.NODE_ENV === 'development';
return (
<div style={{ padding: '20px', border: '2px solid red' }}>
<h2>💥 Error Occurred</h2>
{isDevelopment && (
<>
<details style={{ whiteSpace: 'pre-wrap' }}>
<summary>Error Details</summary>
<p><strong>Error:</strong> {this.state.error?.toString()}</p>
<p><strong>Stack Trace:</strong></p>
<pre>{this.state.errorInfo?.componentStack}</pre>
</details>
</>
)}
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
🎨 Custom Fallback Component
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
}
render() {
if (this.state.hasError) {
// Use custom fallback if provided
return this.props.fallback || <DefaultErrorUI />;
}
return this.props.children;
}
}
// Custom fallback components
function MinimalErrorUI() {
return <div>⚠️ Error occurred</div>;
}
function FriendlyErrorUI() {
return (
<div className="friendly-error">
<h2>😔 Oops!</h2>
<p>Something unexpected happened.</p>
<button onClick={() => window.location.href = '/'}>
Go Home
</button>
</div>
);
}
// Usage with custom fallback
<ErrorBoundary fallback={<FriendlyErrorUI />}>
<MyComponent />
</ErrorBoundary>
🎯 Multiple Error Boundaries
function App() {
return (
<div>
{/* Global error boundary */}
<ErrorBoundary fallback={<AppErrorFallback />}>
<Header />
{/* Sidebar error boundary */}
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
{/* Main content error boundary */}
<ErrorBoundary fallback={<ContentError />}>
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
</div>
);
}
// Benefits:
// 1. Isolated failures - sidebar error won't crash main content
// 2. Specific fallback UI for each section
// 3. Better user experience - rest of app still works
📊 Error Logging Service Integration
// npm install @sentry/react
import * as Sentry from '@sentry/react';
// Initialize Sentry
Sentry.init({
dsn: 'your-sentry-dsn',
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, eventId: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log to Sentry
Sentry.withScope((scope) => {
scope.setExtras(errorInfo);
const eventId = Sentry.captureException(error);
this.setState({ eventId });
});
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<button
onClick={() => {
Sentry.showReportDialog({
eventId: this.state.eventId
});
}}
>
Report Feedback
</button>
</div>
);
}
return this.props.children;
}
}
// Or use Sentry's built-in error boundary
const SentryErrorBoundary = Sentry.ErrorBoundary;
function App() {
return (
<SentryErrorBoundary fallback={<ErrorFallback />}>
<MyApp />
</SentryErrorBoundary>
);
}
⚠️ What Error Boundaries DON'T Catch
// ❌ Error boundaries DO NOT catch:
// 1. Event handlers (use try-catch)
function MyComponent() {
const handleClick = () => {
try {
throw new Error('Event handler error');
} catch (error) {
console.error(error);
}
};
return <button onClick={handleClick}>Click</button>;
}
// 2. Asynchronous code (use try-catch)
function MyComponent() {
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/data');
// ...
} catch (error) {
console.error(error);
}
};
fetchData();
}, []);
}
// 3. Server-side rendering
// 4. Errors in error boundary itself
// ✅ Error boundaries DO catch:
// - Rendering errors
// - Lifecycle method errors
// - Constructor errors in child components
🔧 react-error-boundary Library
// npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
// Simple fallback
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Usage
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset app state
}}
onError={(error, errorInfo) => {
// Log error
console.error(error, errorInfo);
}}
>
<MyApp />
</ErrorBoundary>
);
}
// With reset keys
function User({ userId }) {
const { data: user } = useQuery(['user', userId], fetchUser);
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[userId]} // Reset when userId changes
>
<UserProfile user={user} />
</ErrorBoundary>
);
}
// useErrorHandler hook
import { useErrorHandler } from 'react-error-boundary';
function MyComponent() {
const handleError = useErrorHandler();
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const data = await response.json();
} catch (error) {
handleError(error); // Trigger error boundary
}
};
return <button onClick={fetchData}>Fetch</button>;
}
🎯 Best Practices
- Multiple boundaries: Use separate boundaries for different UI sections
- Granular placement: Place boundaries strategically, not just at the top level
- Log errors: Send errors to monitoring service (Sentry, LogRocket)
- User-friendly fallbacks: Show helpful error messages and recovery options
- Reset functionality: Allow users to recover without full page reload
- Development details: Show stack traces in development, hide in production
- Handle async errors: Use try-catch for promises and async functions
- Test error boundaries: Verify they work by throwing test errors
📋 Complete Example
import { ErrorBoundary } from 'react-error-boundary';
import * as Sentry from '@sentry/react';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="error-container">
<h2>Application Error</h2>
<p>We're experiencing technical difficulties.</p>
<details>
<summary>Error details</summary>
<pre>{error.message}</pre>
</details>
<button onClick={resetErrorBoundary}>Try Again</button>
<button onClick={() => window.location.href = '/'}>Go Home</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
// Log to Sentry
Sentry.captureException(error, { extra: errorInfo });
}}
onReset={() => {
// Reset app state
window.location.reload();
}}
>
<Router>
<Header />
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</ErrorBoundary>
<Footer />
</Router>
</ErrorBoundary>
);
}
🎯 Key Takeaways
- Class components: Error boundaries must be class components
- getDerivedStateFromError: Update state for fallback UI
- componentDidCatch: Log errors and side effects
- Granular boundaries: Multiple boundaries for better isolation
- Event handlers: Use try-catch, not error boundaries
- Async code: Use try-catch for promises/async functions
- react-error-boundary: Simplifies error boundary implementation
- Monitoring: Integrate with Sentry or similar services