Error Handling and Error Boundaries
1. Error Boundary Components and Implementation
| Concept | Description | Catches | Does NOT Catch |
|---|---|---|---|
| Error Boundary | Class component that catches errors in child tree | Render errors, lifecycle errors | Event handlers, async, SSR |
| Placement | Wrap components that might error | All descendants | Errors in boundary itself |
| Granularity | Multiple boundaries for isolation | Specific sections only | Unrelated components |
| Error Type | Caught by Boundary? | Handling Approach |
|---|---|---|
| Render errors | ✅ Yes | Error boundary |
| Lifecycle methods | ✅ Yes | Error boundary |
| Event handlers | ❌ No | try/catch in handler |
| Async code (setTimeout) | ❌ No | try/catch in callback |
| Server-side rendering | ❌ No | Server error handling |
| Error in boundary itself | ❌ No | Parent error boundary |
Example: Error boundary implementation
// 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 so next render shows fallback UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to reporting service
console.error('Error caught by boundary:', error, errorInfo);
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h1>Something went wrong.</h1>
<details>
<summary>Error details</summary>
<pre>{this.state.error.toString()}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
// Usage - wrap components
const App = () => (
<ErrorBoundary>
<Header />
<Main />
<Footer />
</ErrorBoundary>
);
// Multiple boundaries for isolation
const Dashboard = () => (
<div>
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary>
<MainContent />
</ErrorBoundary>
<ErrorBoundary>
<Widgets />
</ErrorBoundary>
</div>
);
// Enhanced error boundary with retry
class ErrorBoundaryWithRetry 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 });
console.error('Error:', error);
console.error('Component stack:', errorInfo.componentStack);
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>Oops! Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.handleReset}>
Try Again
</button>
{process.env.NODE_ENV === 'development' && (
<details>
<summary>Stack trace</summary>
<pre>{this.state.errorInfo?.componentStack}</pre>
</details>
)}
</div>
);
}
return this.props.children;
}
}
// Custom fallback component
const ErrorFallback = ({ error, resetError }) => (
<div role="alert">
<h2>Something went wrong</h2>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetError}>Try again</button>
</div>
);
class ErrorBoundaryWithFallback extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
this.props.onError?.(error, errorInfo);
}
resetError = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
const FallbackComponent = this.props.fallback || ErrorFallback;
return (
<FallbackComponent
error={this.state.error}
resetError={this.resetError}
/>
);
}
return this.props.children;
}
}
// Usage with custom fallback
const App = () => (
<ErrorBoundaryWithFallback
fallback={CustomErrorPage}
onError={(error, info) => logToService(error, info)}
>
<Routes />
</ErrorBoundaryWithFallback>
);
// Component that will error
const BuggyComponent = () => {
const [count, setCount] = useState(0);
if (count > 3) {
throw new Error('Count exceeded limit!');
}
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
};
Warning: Error boundaries do NOT catch errors in event handlers, async code (setTimeout,
promises),
server-side rendering, or errors thrown in the error boundary itself. Use try/catch for those cases.
2. componentDidCatch and getDerivedStateFromError
| Method | When Called | Purpose | Can Update State |
|---|---|---|---|
| getDerivedStateFromError | During render phase | Update state to show fallback UI | ✅ Yes (return new state) |
| componentDidCatch | After commit phase | Log errors, report to service | ⚠️ Yes but triggers extra render |
| Parameter | Available In | Contains |
|---|---|---|
| error | Both methods | Error object with message, stack |
| errorInfo | componentDidCatch only | componentStack - where error occurred |
Example: Using both lifecycle methods
class AdvancedErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorCount: 0
};
}
// Static method - called during render phase
// MUST be pure - no side effects!
static getDerivedStateFromError(error) {
// Update state to trigger fallback UI
return {
hasError: true,
error: error
};
}
// Called after render in commit phase
// Can have side effects - logging, reporting
componentDidCatch(error, errorInfo) {
// errorInfo.componentStack shows component hierarchy
console.error('Error caught:', error);
console.error('Component stack:', errorInfo.componentStack);
// Update state to store error details
this.setState(prevState => ({
errorInfo,
errorCount: prevState.errorCount + 1
}));
// Report to error tracking service
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// Send to monitoring service
// Sentry.captureException(error, { contexts: { react: errorInfo } });
}
componentDidUpdate(prevProps, prevState) {
// Clear error if children change (new route, etc)
if (this.state.hasError && prevProps.children !== this.props.children) {
this.setState({ hasError: false, error: null, errorInfo: null });
}
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h1>Something went wrong</h1>
<p>{this.state.error?.message}</p>
<p>Error count: {this.state.errorCount}</p>
{process.env.NODE_ENV === 'development' && (
<>
<h3>Error Stack:</h3>
<pre>{this.state.error?.stack}</pre>
<h3>Component Stack:</h3>
<pre>{this.state.errorInfo?.componentStack}</pre>
</>
)}
<button onClick={this.handleReset}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// Error boundary with different fallbacks based on error type
class SmartErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Categorize errors
const errorType = this.categorizeError(error);
console.error(\`[\${errorType}] Error:`, error);
console.error('Stack:', errorInfo.componentStack);
// Report with category
this.reportError(error, errorInfo, errorType);
}
categorizeError(error) {
if (error.message.includes('fetch')) return 'NETWORK';
if (error.message.includes('undefined')) return 'RUNTIME';
if (error.name === 'ChunkLoadError') return 'CHUNK_LOAD';
return 'UNKNOWN';
}
reportError(error, errorInfo, category) {
// Send to monitoring service
fetch('/api/errors', {
method: 'POST',
body: JSON.stringify({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
category,
timestamp: Date.now(),
userAgent: navigator.userAgent
})
});
}
render() {
if (this.state.hasError) {
const errorType = this.categorizeError(this.state.error);
// Different UI based on error type
switch (errorType) {
case 'NETWORK':
return <NetworkErrorFallback />;
case 'CHUNK_LOAD':
return <ChunkLoadErrorFallback />;
default:
return <GenericErrorFallback error={this.state.error} />;
}
}
return this.props.children;
}
}
Note: Use
getDerivedStateFromError for UI updates (pure), and
componentDidCatch for side effects like logging. getDerivedStateFromError is called during
render, so it must be pure.
3. useErrorHandler Hook Patterns
| Pattern | Description | Use Case |
|---|---|---|
| Custom hook | Throw errors to boundary from hooks | Event handlers, async operations |
| Error state | Store error in state, throw in render | Propagate to error boundary |
| Library (react-error-boundary) | Pre-built hooks and components | Production-ready solution |
Example: useErrorHandler custom hook
// Custom useErrorHandler hook
const useErrorHandler = () => {
const [error, setError] = useState(null);
// Throw error during render to trigger error boundary
if (error) {
throw error;
}
return setError;
};
// Usage in event handlers
const MyComponent = () => {
const handleError = useErrorHandler();
const handleClick = async () => {
try {
await somethingThatMightFail();
} catch (error) {
handleError(error); // Propagates to error boundary
}
};
return <button onClick={handleClick}>Click me</button>;
};
// Using with data fetching
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const handleError = useErrorHandler();
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(\`/api/users/\${userId}\`);
if (!response.ok) throw new Error('Failed to fetch user');
const data = await response.json();
setUser(data);
} catch (error) {
handleError(error); // Triggers error boundary
}
};
fetchUser();
}, [userId, handleError]);
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
};
// Using react-error-boundary library
import { useErrorHandler, ErrorBoundary } from 'react-error-boundary';
const DataFetcher = () => {
const handleError = useErrorHandler();
useEffect(() => {
fetchData()
.catch(handleError); // Automatically propagates to boundary
}, [handleError]);
return <div>Data</div>;
};
// Error boundary with hook from library
const App = () => (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
console.error('Error:', error);
console.error('Info:', errorInfo);
}}
onReset={() => {
// Reset app state
window.location.href = '/';
}}
>
<DataFetcher />
</ErrorBoundary>
);
// Advanced hook with error context
const ErrorContext = createContext();
const ErrorProvider = ({ children }) => {
const [errors, setErrors] = useState([]);
const addError = useCallback((error) => {
setErrors(prev => [...prev, {
id: Date.now(),
message: error.message,
timestamp: new Date()
}]);
}, []);
const clearErrors = useCallback(() => {
setErrors([]);
}, []);
return (
<ErrorContext.Provider value={{ errors, addError, clearErrors }}>
{children}
</ErrorContext.Provider>
);
};
const useError = () => {
const context = useContext(ErrorContext);
if (!context) {
throw new Error('useError must be used within ErrorProvider');
}
return context;
};
// Usage
const MyComponent = () => {
const { addError, errors } = useError();
const handleAction = async () => {
try {
await riskyOperation();
} catch (error) {
addError(error);
}
};
return (
<div>
{errors.map(err => (
<div key={err.id} className="error-toast">
{err.message}
</div>
))}
<button onClick={handleAction}>Action</button>
</div>
);
};
// Retrying with hook
const useAsyncError = () => {
const [, setError] = useState();
return useCallback(
(error) => {
setError(() => {
throw error;
});
},
[]
);
};
const ComponentWithRetry = () => {
const throwError = useAsyncError();
const [retryCount, setRetryCount] = useState(0);
const fetchData = async () => {
try {
const data = await fetch('/api/data');
if (!data.ok) throw new Error('Fetch failed');
return data.json();
} catch (error) {
if (retryCount < 3) {
setRetryCount(retryCount + 1);
} else {
throwError(error);
}
}
};
useEffect(() => {
fetchData();
}, [retryCount]);
return <div>Content</div>;
};
Note: Consider using the
react-error-boundary library for production apps.
It provides battle-tested hooks and components with advanced features like retry, reset keys, and more.
4. Async Error Handling and Promise Rejections
| Async Scenario | Error Boundary? | Solution |
|---|---|---|
| useEffect fetch | ❌ No | try/catch + error state or hook |
| Event handler async | ❌ No | try/catch + error state |
| setTimeout/setInterval | ❌ No | try/catch in callback |
| Promise.catch | ❌ No | .catch() or try/catch with await |
| Unhandled rejection | ❌ No | Global handler + error boundary |
Example: Async error handling patterns
// Async/await with try/catch
const DataComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(\`HTTP error! status: \${response.status}\`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
console.error('Fetch error:', err);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
};
// Promise chains with .catch()
const PromiseChainComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => setData(data))
.catch(error => {
console.error('Error:', error);
setError(error.message);
});
}, []);
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
};
// Global unhandled rejection handler
const setupGlobalErrorHandling = () => {
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Prevent default browser handling
event.preventDefault();
// Report to monitoring service
reportError({
type: 'unhandled_rejection',
error: event.reason,
promise: event.promise
});
});
// Global error handler
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
reportError({
type: 'global_error',
error: event.error,
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
};
// Call in app initialization
setupGlobalErrorHandling();
// Async error with custom hook
const useAsyncOperation = (asyncFn) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const execute = useCallback(async (...args) => {
try {
setLoading(true);
setError(null);
const result = await asyncFn(...args);
setData(result);
return result;
} catch (err) {
setError(err);
throw err; // Re-throw if caller wants to handle
} finally {
setLoading(false);
}
}, [asyncFn]);
return { data, error, loading, execute };
};
// Usage
const MyComponent = () => {
const { data, error, loading, execute } = useAsyncOperation(
async (id) => {
const response = await fetch(\`/api/users/\${id}\`);
if (!response.ok) throw new Error('Fetch failed');
return response.json();
}
);
const handleClick = async () => {
try {
await execute(123);
} catch (err) {
console.error('Operation failed:', err);
}
};
return (
<div>
<button onClick={handleClick} disabled={loading}>
Fetch User
</button>
{loading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
{data && <div>{data.name}</div>}
</div>
);
};
// Timeout with error handling
const ComponentWithTimeout = () => {
const [message, setMessage] = useState('');
const handleError = useErrorHandler();
useEffect(() => {
const timerId = setTimeout(() => {
try {
// Risky operation
riskyOperation();
setMessage('Success');
} catch (error) {
handleError(error);
}
}, 1000);
return () => clearTimeout(timerId);
}, [handleError]);
return <div>{message}</div>;
};
// Parallel requests with error handling
const MultipleRequests = () => {
const [data, setData] = useState(null);
const [errors, setErrors] = useState([]);
useEffect(() => {
const fetchAll = async () => {
const urls = ['/api/data1', '/api/data2', '/api/data3'];
// Promise.allSettled to handle partial failures
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
setData(successful);
setErrors(failed);
};
fetchAll();
}, []);
return (
<div>
{errors.length > 0 && (
<div>
{errors.length} requests failed
</div>
)}
{data && <div>{JSON.stringify(data)}</div>}
</div>
);
};
Warning: Always handle promise rejections! Unhandled rejections can cause silent failures.
Use try/catch with async/await or .catch() with promises. Set up global handlers as a safety net.
5. Error Reporting and Monitoring Integration
| Service | Features | Integration |
|---|---|---|
| Sentry | Error tracking, performance, releases | @sentry/react SDK |
| LogRocket | Session replay, error tracking | logrocket + logrocket-react |
| Bugsnag | Error monitoring, releases | @bugsnag/js + @bugsnag/plugin-react |
| Rollbar | Real-time error tracking | rollbar package |
Example: Error monitoring integration
// Sentry integration
import * as Sentry from '@sentry/react';
// Initialize Sentry
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
environment: process.env.NODE_ENV,
release: process.env.REACT_APP_VERSION,
tracesSampleRate: 1.0, // Capture 100% of transactions
integrations: [
new Sentry.BrowserTracing(),
new Sentry.Replay()
],
beforeSend(event, hint) {
// Modify or filter events before sending
if (event.exception) {
console.error('Sending error to Sentry:', hint.originalException);
}
return event;
}
});
// Use Sentry's error boundary
const App = () => (
<Sentry.ErrorBoundary
fallback={({ error, resetError }) => (
<div>
<h1>An error occurred</h1>
<p>{error.message}</p>
<button onClick={resetError}>Try again</button>
</div>
)}
showDialog
>
<Routes />
</Sentry.ErrorBoundary>
);
// Manual error capture
const handleError = (error) => {
Sentry.captureException(error, {
tags: {
component: 'UserProfile',
action: 'fetchUser'
},
level: 'error',
extra: {
userId: user.id,
timestamp: Date.now()
}
});
};
// Add user context
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username
});
// Add breadcrumbs for debugging
Sentry.addBreadcrumb({
category: 'user-action',
message: 'User clicked submit button',
level: 'info'
});
// LogRocket integration
import LogRocket from 'logrocket';
import setupLogRocketReact from 'logrocket-react';
// Initialize LogRocket
LogRocket.init('your-app-id/project-name', {
release: process.env.REACT_APP_VERSION,
console: {
shouldAggregateConsoleErrors: true
}
});
setupLogRocketReact(LogRocket);
// Identify users
LogRocket.identify(user.id, {
name: user.name,
email: user.email,
subscriptionType: user.plan
});
// LogRocket with Sentry
LogRocket.getSessionURL((sessionURL) => {
Sentry.configureScope((scope) => {
scope.setExtra('sessionURL', sessionURL);
});
});
// Custom error reporting service
class ErrorReporter {
static async report(error, errorInfo, context = {}) {
const errorData = {
message: error.message,
stack: error.stack,
componentStack: errorInfo?.componentStack,
context,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString(),
user: this.getUserContext()
};
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
});
} catch (reportError) {
console.error('Failed to report error:', reportError);
}
}
static getUserContext() {
// Get user info from your auth system
return {
id: localStorage.getItem('userId'),
sessionId: localStorage.getItem('sessionId')
};
}
}
// Use in error boundary
class MonitoredErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Report to multiple services
ErrorReporter.report(error, errorInfo, {
component: this.props.name
});
Sentry.captureException(error, {
contexts: { react: errorInfo }
});
LogRocket.captureException(error, {
extra: errorInfo
});
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
// Automatic error reporting with fetch interceptor
const originalFetch = window.fetch;
window.fetch = async (...args) => {
try {
const response = await originalFetch(...args);
if (!response.ok) {
Sentry.captureMessage(\`HTTP Error: \${response.status}\`, {
level: 'warning',
extra: {
url: args[0],
status: response.status
}
});
}
return response;
} catch (error) {
Sentry.captureException(error, {
tags: { type: 'fetch-error' }
});
throw error;
}
};
Note: Choose an error monitoring service that fits your needs. Sentry is popular for error
tracking, LogRocket adds session replay, and both provide valuable debugging context. Always sanitize sensitive
data before reporting.
6. Fallback UI Components and Recovery Patterns
| Pattern | Description | User Experience |
|---|---|---|
| Full page fallback | Replace entire app with error page | Critical errors only |
| Section fallback | Replace section with error message | Rest of app still works |
| Inline error | Show error inline in component | Minimal disruption |
| Retry button | Let user retry failed operation | Self-service recovery |
| Automatic retry | Retry with exponential backoff | Seamless recovery attempt |
| Graceful degradation | Show partial content/reduced features | Better than nothing |
Example: Fallback UI and recovery patterns
// Simple error fallback
const ErrorFallback = ({ error, resetError }) => (
<div className="error-container">
<h2>⚠️ Something went wrong</h2>
<p>{error.message}</p>
<button onClick={resetError}>Try again</button>
</div>
);
// Feature-specific fallback
const FeatureFallback = ({ error, resetError, featureName }) => (
<div className="feature-error">
<p>Unable to load {featureName}</p>
<button onClick={resetError}>Retry</button>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
// Graceful degradation example
const DataList = ({ items }) => {
const [error, setError] = useState(null);
const [enhancedData, setEnhancedData] = useState(null);
useEffect(() => {
// Try to enhance data with additional info
const enhance = async () => {
try {
const enhanced = await fetchEnhancedData(items);
setEnhancedData(enhanced);
} catch (err) {
setError(err);
// Continue with basic data
}
};
enhance();
}, [items]);
// Show basic data if enhancement fails
const displayData = enhancedData || items;
return (
<div>
{error && (
<div className="warning">
Showing basic view (enhanced features unavailable)
</div>
)}
{displayData.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};
// Error boundary with reset key
const App = () => {
const [resetKey, setResetKey] = useState(0);
return (
<ErrorBoundary
resetKey={resetKey} // Changes when state updates
onReset={() => setResetKey(k => k + 1)}
FallbackComponent={ErrorFallback}
>
<Routes />
</ErrorBoundary>
);
};
// Automatic retry with exponential backoff
const useRetryableAsync = (asyncFn, maxRetries = 3) => {
const [state, setState] = useState({
data: null,
error: null,
loading: false,
retryCount: 0
});
const execute = useCallback(async (...args) => {
setState(s => ({ ...s, loading: true, error: null }));
for (let i = 0; i <= maxRetries; i++) {
try {
const result = await asyncFn(...args);
setState({
data: result,
error: null,
loading: false,
retryCount: i
});
return result;
} catch (error) {
if (i === maxRetries) {
setState({
data: null,
error,
loading: false,
retryCount: i
});
throw error;
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}, [asyncFn, maxRetries]);
return { ...state, execute };
};
// Usage
const DataComponent = () => {
const { data, error, loading, retryCount, execute } = useRetryableAsync(
async () => {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Fetch failed');
return response.json();
}
);
useEffect(() => {
execute();
}, [execute]);
if (loading) return <div>Loading... (attempt {retryCount + 1}/3)</div>;
if (error) return <div>Failed after {retryCount} retries</div>;
return <div>{JSON.stringify(data)}</div>;
};
// Chunk load error recovery (code splitting)
const ChunkLoadErrorFallback = () => (
<div className="chunk-error">
<h2>Update Available</h2>
<p>A new version of the app is available.</p>
<button onClick={() => window.location.reload()}>
Reload to Update
</button>
</div>
);
// Network error fallback
const NetworkErrorFallback = ({ resetError }) => (
<div className="network-error">
<h2>🌐 Connection Issue</h2>
<p>Please check your internet connection.</p>
<button onClick={resetError}>Retry</button>
</div>
);
// Multiple error boundaries for granular fallbacks
const Dashboard = () => (
<div>
<Header /> {/* Outside boundaries - always shown */}
<div className="dashboard-content">
<ErrorBoundary FallbackComponent={SidebarError}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={MainContentError}>
<MainContent />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetError}>
<Widgets />
</ErrorBoundary>
</div>
</div>
);