Error Boundaries in React
Learn how to catch and handle errors in React component trees using Error Boundaries to prevent entire applications from crashing.
Topics Covered:
Prerequisites:
- Understanding Props
- Managing State with useState
- Understanding useEffect
Video Tutorial
Overview
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 application. They're essential for building robust production applications. This tutorial covers how to create Error Boundaries, handle different types of errors, and implement error recovery strategies.
Understanding Error Boundaries
Error Boundaries are React components that catch errors in their child component tree. They're like try-catch blocks, but for React components. What Error Boundaries Do: • Catch errors during rendering • Catch errors in lifecycle methods • Catch errors in constructors • Display fallback UI • Log errors for debugging What They DON'T Catch: • Errors in event handlers • Errors in async code (setTimeout, promises) • Errors during server-side rendering • Errors in the Error Boundary itself Why We Need Them: • Prevent entire app from crashing • Provide better user experience • Isolate errors to specific parts • Enable error recovery
// Basic Error Boundary (class component)
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Update state to show fallback UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught by boundary:', error, errorInfo);
// You can log to service like Sentry, LogRocket, etc.
}
render() {
if (this.state.hasError) {
// Fallback UI
return (
<div>
<h2>Something went wrong.</h2>
<details>
{this.state.error && this.state.error.toString()}
</details>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}
// Error Boundary with recovery
class ErrorBoundaryWithRecovery extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorCount: 0 };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
this.setState(prev => ({
errorCount: prev.errorCount + 1
}));
console.error('Error:', error, errorInfo);
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<p>Error count: {this.state.errorCount}</p>
<button onClick={this.handleReset}>Try again</button>
</div>
);
}
return this.props.children;
}
}Error Boundaries are class components that implement getDerivedStateFromError and componentDidCatch. They catch errors in their child tree and display fallback UI instead of crashing the app.
Creating Error Boundaries
Error Boundaries must be class components (as of React 16+). However, you can create reusable Error Boundary components and use them throughout your app. Error Boundary Requirements: • Must be a class component • Must implement getDerivedStateFromError • Should implement componentDidCatch • Can have custom fallback UI • Can support error recovery Best Practices: • Create reusable Error Boundary components • Place them strategically in component tree • Provide helpful error messages • Log errors to monitoring service • Allow error recovery when possible
// Reusable Error Boundary component
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error) {
return {
hasError: true,
error
};
}
componentDidCatch(error, errorInfo) {
this.setState({
errorInfo
});
// Log to error reporting service
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback(this.state.error, this.handleReset);
}
return (
<div className="error-boundary">
<h2>{this.props.title || 'Something went wrong'}</h2>
{this.props.showDetails && (
<details>
<summary>Error details</summary>
<pre>{this.state.error?.toString()}</pre>
<pre>{this.state.errorInfo?.componentStack}</pre>
</details>
)}
{this.props.onReset && (
<button onClick={this.handleReset}>
{this.props.resetButtonText || 'Try again'}
</button>
)}
</div>
);
}
return this.props.children;
}
}
// Usage with custom fallback
function App() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<div>
<h2>Custom Error UI</h2>
<button onClick={reset}>Reset</button>
</div>
)}
onError={(error, errorInfo) => {
// Send to error tracking service
console.error('Error:', error, errorInfo);
}}
>
<MyComponent />
</ErrorBoundary>
);
}
// Multiple Error Boundaries for different sections
function App() {
return (
<div>
<ErrorBoundary title="Header Error">
<Header />
</ErrorBoundary>
<ErrorBoundary title="Sidebar Error">
<Sidebar />
</ErrorBoundary>
<ErrorBoundary title="Main Content Error">
<MainContent />
</ErrorBoundary>
</div>
);
}Create reusable Error Boundary components with customizable fallback UI and error handling. Place them strategically throughout your app to isolate errors to specific sections.
Error Boundary Patterns
There are several patterns for using Error Boundaries effectively in your application. Common Patterns: • Top-level Error Boundary (catches all errors) • Section-level Error Boundaries (isolate errors) • Feature-level Error Boundaries (per feature) • Component-level Error Boundaries (wrap risky components) When to Use Each: • Top-level: Catch-all safety net • Section-level: Isolate major app sections • Feature-level: Isolate features • Component-level: Wrap components that might fail Best Practices: • Use multiple boundaries at different levels • Provide context-specific error messages • Allow partial app functionality • Log errors appropriately
// Pattern 1: Top-level boundary
function App() {
return (
<ErrorBoundary>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
</ErrorBoundary>
);
}
// Pattern 2: Section-level boundaries
function Dashboard() {
return (
<div>
<ErrorBoundary title="Navigation Error">
<Navigation />
</ErrorBoundary>
<ErrorBoundary title="Content Error">
<DashboardContent />
</ErrorBoundary>
<ErrorBoundary title="Sidebar Error">
<Sidebar />
</ErrorBoundary>
</div>
);
}
// Pattern 3: Feature-level boundaries
function UserProfile({ userId }) {
return (
<ErrorBoundary>
<UserHeader userId={userId} />
<ErrorBoundary title="Posts Error">
<UserPosts userId={userId} />
</ErrorBoundary>
<ErrorBoundary title="Friends Error">
<UserFriends userId={userId} />
</ErrorBoundary>
</ErrorBoundary>
);
}
// Pattern 4: Component-level (risky components)
function DataVisualization({ data }) {
return (
<ErrorBoundary title="Chart Error">
<ComplexChart data={data} />
</ErrorBoundary>
);
}
// Pattern 5: Nested boundaries
function App() {
return (
<ErrorBoundary title="App Error">
<Header />
<ErrorBoundary title="Content Error">
<MainContent>
<ErrorBoundary title="Widget Error">
<RiskyWidget />
</ErrorBoundary>
</MainContent>
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}Use Error Boundaries at different levels: top-level for safety net, section-level to isolate major parts, feature-level for features, and component-level for risky components. This provides graceful degradation.
Handling Different Error Types
Different types of errors require different handling strategies. Understanding what Error Boundaries catch and what they don't is crucial. What Error Boundaries Catch: • Errors during render • Errors in lifecycle methods • Errors in constructors • Errors in child components What They DON'T Catch: • Errors in event handlers (use try-catch) • Errors in async code (use try-catch) • Errors in setTimeout/setInterval • Errors during server-side rendering • Errors in the Error Boundary itself Handling Strategies: • Use Error Boundaries for render errors • Use try-catch for event handlers • Use try-catch for async operations • Combine both approaches
// Error Boundary catches render errors
function ComponentWithRenderError() {
const data = null;
return <div>{data.items.map(...)}</div>; // Error caught by boundary
}
// ❌ Error Boundary does NOT catch event handler errors
function ComponentWithEventHandlerError() {
const handleClick = () => {
throw new Error('Event handler error'); // NOT caught by boundary
};
return <button onClick={handleClick}>Click</button>;
}
// ✅ Use try-catch for event handlers
function SafeEventHandler() {
const handleClick = () => {
try {
// Risky operation
riskyOperation();
} catch (error) {
console.error('Error in handler:', error);
// Show error to user
}
};
return <button onClick={handleClick}>Click</button>;
}
// ❌ Error Boundary does NOT catch async errors
function ComponentWithAsyncError() {
useEffect(() => {
fetch('/api/data')
.then(() => {
throw new Error('Async error'); // NOT caught by boundary
});
}, []);
return <div>Content</div>;
}
// ✅ Handle async errors properly
function SafeAsyncComponent() {
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
// Process data
})
.catch(err => {
setError(err); // Handle error in state
});
}, []);
if (error) {
return <div>Error: {error.message}</div>;
}
return <div>Content</div>;
}
// Combined approach: Error Boundary + try-catch
function RobustComponent() {
const [error, setError] = useState(null);
const handleAsyncAction = async () => {
try {
await riskyAsyncOperation();
} catch (err) {
setError(err);
}
};
// Render errors caught by Error Boundary
if (someCondition) {
throw new Error('Render error'); // Caught by boundary
}
return (
<div>
{error && <div>Action error: {error.message}</div>}
<button onClick={handleAsyncAction}>Action</button>
</div>
);
}Error Boundaries only catch render/lifecycle errors. Use try-catch for event handlers and async code. Combine both approaches for comprehensive error handling.
Error Logging and Monitoring
Logging errors to monitoring services is crucial for production applications. Error Boundaries provide the perfect place to integrate error logging. Error Logging Services: • Sentry - Popular error tracking • LogRocket - Session replay + errors • Bugsnag - Error monitoring • Rollbar - Error tracking • Custom logging service What to Log: • Error message and stack trace • Component stack • User context • Browser information • User actions leading to error Best Practices: • Log to external service • Don't log sensitive data • Include helpful context • Set up alerts for critical errors • Track error frequency
// Error Boundary with Sentry integration
import * as Sentry from '@sentry/react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log to Sentry
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack
}
},
tags: {
errorBoundary: true
},
extra: {
errorInfo
}
});
// Also log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Error caught by boundary:', error, errorInfo);
}
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
// Error Boundary with custom logging
class ErrorBoundaryWithLogging extends React.Component {
componentDidCatch(error, errorInfo) {
// Log to custom service
this.logError({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
userId: this.getUserId(), // If available
});
}
logError = async (errorData) => {
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
});
} catch (err) {
console.error('Failed to log error:', err);
}
};
getUserId = () => {
// Get user ID from context, state, etc.
return null;
};
// ... rest of Error Boundary
}
// Error Boundary with error reporting hook
function useErrorReporting() {
const reportError = useCallback((error, errorInfo) => {
// Send to multiple services
Sentry.captureException(error, { contexts: { react: errorInfo } });
// Custom logging
console.error('Error:', error);
// Analytics
analytics.track('error_occurred', {
error_message: error.message,
error_type: error.name
});
}, []);
return { reportError };
}
class ErrorBoundary extends React.Component {
// ... use useErrorReporting hook
}Integrate error logging into Error Boundaries. Use services like Sentry for production error tracking. Log helpful context but avoid sensitive data. Set up alerts for critical errors.
Error Recovery Strategies
Allowing users to recover from errors improves user experience. Error Boundaries can support various recovery strategies. Recovery Strategies: • Reset button - Clear error state • Retry mechanism - Try operation again • Fallback to cached data • Redirect to safe page • Show partial content • Allow user to continue Implementation: • Store recovery function in state • Provide UI for recovery • Reset error state • Re-render component tree Best Practices: • Always provide recovery option when possible • Make recovery action clear • Preserve user data when possible • Log recovery attempts
// Error Boundary with reset
class ErrorBoundaryWithReset extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={this.handleReset}>Try again</button>
</div>
);
}
return this.props.children;
}
}
// Error Boundary with retry
class ErrorBoundaryWithRetry extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
retryCount: 0
};
}
static getDerivedStateFromError(error) {
return { hasError: true, error: error };
}
handleRetry = () => {
this.setState(prev => ({
hasError: false,
error: null,
retryCount: prev.retryCount + 1
}));
};
render() {
if (this.state.hasError) {
return (
<div>
<h2>Error occurred</h2>
<p>Retry count: {this.state.retryCount}</p>
<button onClick={this.handleRetry}>
Retry
</button>
</div>
);
}
return this.props.children;
}
}
// Error Boundary with fallback content
class ErrorBoundaryWithFallback extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback || <DefaultFallback />;
}
return this.props.children;
}
}
// Usage with fallback
function App() {
return (
<ErrorBoundaryWithFallback
fallback={
<div>
<h2>Content unavailable</h2>
<p>Please try again later</p>
<Link to="/">Go to home</Link>
</div>
}
>
<RiskyComponent />
</ErrorBoundaryWithFallback>
);
}
// Error Boundary with key-based reset
function App() {
const [key, setKey] = useState(0);
const handleReset = () => {
setKey(prev => prev + 1); // Force remount
};
return (
<ErrorBoundary
key={key}
onReset={handleReset}
>
<RiskyComponent />
</ErrorBoundary>
);
}Implement error recovery by allowing users to reset error state, retry operations, or navigate away. Use key prop to force component remount for complete reset. Always provide clear recovery options.
Conclusion
Error Boundaries are essential for building robust React applications. They catch render and lifecycle errors, display fallback UI, and prevent entire apps from crashing. Use them at multiple levels, integrate error logging, and provide recovery options. Remember: Error Boundaries only catch render/lifecycle errors - use try-catch for event handlers and async code. Combine both approaches for comprehensive error handling.