Error Boundary Strategies
Granular error isolation with strategic error boundary placement
Error Boundary Strategies
Error boundaries prevent one component's error from crashing your entire application. Strategic placement creates resilient apps with isolated failure zones.
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.
What they catch:
- Rendering errors
- Lifecycle method errors
- Constructor errors in child components
What they DON'T catch:
- Event handlers (use try-catch)
- Asynchronous code (use error states)
- Server-side rendering errors
- Errors in the error boundary itself
Basic Error Boundary
// components/ErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
export 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) {
// Log to error reporting service
console.error('Error boundary caught:', error, errorInfo);
// Call custom error handler
this.props.onError?.(error, errorInfo);
}
reset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError && this.state.error) {
// Custom fallback
if (typeof this.props.fallback === 'function') {
return this.props.fallback(this.state.error, this.reset);
}
if (this.props.fallback) {
return this.props.fallback;
}
// Default fallback
return (
<div className="error-boundary-fallback">
<h2>Something went wrong</h2>
<button onClick={this.reset}>Try again</button>
</div>
);
}
return this.props.children;
}
}Granular Boundary Placement
1. App-Level Boundary (Coarse)
// app/layout.tsx
import { ErrorBoundary } from '@/components/ErrorBoundary';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ErrorBoundary fallback={<CriticalError />}>
{children}
</ErrorBoundary>
</body>
</html>
);
}
function CriticalError() {
return (
<div className="critical-error">
<h1>Application Error</h1>
<p>We're sorry, but something went wrong.</p>
<button onClick={() => window.location.reload()}>
Reload Application
</button>
</div>
);
}2. Page-Level Boundaries (Medium)
// app/dashboard/page.tsx
export default function Dashboard() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<PageError error={error} onReset={reset} />
)}
>
<DashboardContent />
</ErrorBoundary>
);
}3. Feature-Level Boundaries (Fine)
// components/Dashboard.tsx
function Dashboard() {
return (
<div className="dashboard">
{/* Each feature is isolated */}
<ErrorBoundary fallback={<ProfileError />}>
<UserProfile />
</ErrorBoundary>
<ErrorBoundary fallback={<NotificationsError />}>
<Notifications />
</ErrorBoundary>
<ErrorBoundary fallback={<AnalyticsError />}>
<Analytics />
</ErrorBoundary>
<ErrorBoundary fallback={<ActivityError />}>
<ActivityFeed />
</ErrorBoundary>
</div>
);
}4. Component-Level Boundaries (Very Fine)
// For critical individual components
function ProductList() {
return (
<div>
{products.map(product => (
<ErrorBoundary key={product.id} fallback={<ProductCardError />}>
<ProductCard product={product} />
</ErrorBoundary>
))}
</div>
);
}Advanced Error Boundary Patterns
Named Boundaries with Context
// components/NamedErrorBoundary.tsx
interface NamedErrorBoundaryProps {
name: string;
children: ReactNode;
fallback?: ReactNode;
}
export class NamedErrorBoundary extends Component<NamedErrorBoundaryProps, State> {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Report with boundary name for better debugging
reportError(error, {
boundary: this.props.name,
componentStack: errorInfo.componentStack,
});
}
// ... rest of implementation
}
// Usage
<NamedErrorBoundary name="UserProfile">
<UserProfile />
</NamedErrorBoundary>Retry with Exponential Backoff
// components/RetryErrorBoundary.tsx
interface State {
hasError: boolean;
error: Error | null;
retryCount: number;
}
export class RetryErrorBoundary extends Component<Props, State> {
state = { hasError: false, error: null, retryCount: 0 };
private maxRetries = 3;
private retryDelay = 1000; // Start with 1s
reset = () => {
const { retryCount } = this.state;
if (retryCount >= this.maxRetries) {
console.error('Max retries reached');
return;
}
// Exponential backoff: 1s, 2s, 4s
const delay = this.retryDelay * Math.pow(2, retryCount);
setTimeout(() => {
this.setState(state => ({
hasError: false,
error: null,
retryCount: state.retryCount + 1,
}));
}, delay);
};
render() {
if (this.state.hasError) {
const canRetry = this.state.retryCount < this.maxRetries;
return (
<div>
<p>Error: {this.state.error?.message}</p>
{canRetry ? (
<button onClick={this.reset}>
Retry ({this.maxRetries - this.state.retryCount} left)
</button>
) : (
<p>Maximum retries reached. Please refresh the page.</p>
)}
</div>
);
}
return this.props.children;
}
}Conditional Boundaries
// Only wrap in error boundary in production
function ConditionalErrorBoundary({ children }: { children: ReactNode }) {
if (process.env.NODE_ENV === 'development') {
// In dev, let errors bubble for better DX
return <>{children}</>;
}
// In production, catch errors
return (
<ErrorBoundary fallback={<ErrorFallback />}>
{children}
</ErrorBoundary>
);
}Cascading Boundaries
// Each level provides more specific fallbacks
function App() {
return (
{/* Level 1: App-wide fallback */}
<ErrorBoundary fallback={<AppError />}>
<Header />
{/* Level 2: Page-level fallback */}
<ErrorBoundary fallback={<PageError />}>
<MainContent>
{/* Level 3: Feature-level fallback */}
<ErrorBoundary fallback={<FeatureError />}>
<FeatureComponent />
</ErrorBoundary>
</MainContent>
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}React 19 Error Boundary Hook
// React 19 introduces useErrorBoundary hook
'use client';
import { useErrorBoundary } from 'react';
function MyComponent() {
const [error, resetError] = useErrorBoundary();
if (error) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={resetError}>Try again</button>
</div>
);
}
return <div>Normal content</div>;
}Error Boundary + Suspense
// Combine error boundaries with Suspense for complete UX
function DataComponent() {
return (
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<LoadingUI />}>
<AsyncData />
</Suspense>
</ErrorBoundary>
);
}
// This handles:
// - Loading state → LoadingUI
// - Error state → ErrorUI
// - Success state → AsyncDataReusable Error Fallbacks
// components/ErrorFallbacks.tsx
export function FeatureUnavailable({ feature, onRetry }: Props) {
return (
<div className="error-fallback feature-unavailable">
<Icon name="warning" />
<h3>{feature} is temporarily unavailable</h3>
<p>We're working on it. Please try again.</p>
<button onClick={onRetry}>Retry</button>
</div>
);
}
export function PartialError({ message, children }: Props) {
return (
<div className="partial-error">
<div className="error-banner">
<Icon name="alert" />
<span>{message}</span>
</div>
{children}
</div>
);
}
export function CriticalError() {
return (
<div className="critical-error">
<Icon name="error" size="large" />
<h1>Something went wrong</h1>
<p>Please refresh the page or contact support if the problem persists.</p>
<div className="actions">
<button onClick={() => window.location.reload()}>
Refresh Page
</button>
<a href="/">Go to Homepage</a>
<a href="/support">Contact Support</a>
</div>
</div>
);
}Testing Error Boundaries
// components/__tests__/ErrorBoundary.test.tsx
import { render, screen } from '@testing-library/react';
import { ErrorBoundary } from '../ErrorBoundary';
const ThrowError = () => {
throw new Error('Test error');
};
describe('ErrorBoundary', () => {
it('catches errors and shows fallback', () => {
// Suppress console.error for this test
const spy = jest.spyOn(console, 'error').mockImplementation();
render(
<ErrorBoundary fallback={<div>Error occurred</div>}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Error occurred')).toBeInTheDocument();
spy.mockRestore();
});
it('resets error when reset is called', () => {
const spy = jest.spyOn(console, 'error').mockImplementation();
const { rerender } = render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>
);
// Error is shown
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
// Click reset
screen.getByText('Try again').click();
// Error is cleared
rerender(
<ErrorBoundary>
<div>Success</div>
</ErrorBoundary>
);
expect(screen.getByText('Success')).toBeInTheDocument();
spy.mockRestore();
});
});Boundary Placement Strategy
| Level | Granularity | Use Case | Fallback |
|---|---|---|---|
| App | Coarse | Critical errors | Full page error |
| Route | Medium | Page-level issues | Page error |
| Feature | Fine | Feature failures | Feature unavailable |
| Component | Very Fine | Individual components | Component error |
Best Practices
- Multiple Layers: Use cascading boundaries for different failure zones
- Named Boundaries: Add names for better debugging
- Specific Fallbacks: Show relevant fallback UI for each boundary
- Enable Recovery: Provide reset/retry mechanisms
- Report Errors: Always log to monitoring service
- Test Boundaries: Include error scenarios in tests
- Don't Overuse: Too many boundaries = degraded UX
Common Mistakes
❌ Single app-level boundary: One error crashes everything
✅ Multiple strategic boundaries
❌ No reset mechanism: Users stuck in error state
✅ Provide retry/reset options
❌ Generic "Something went wrong": Not helpful
✅ Contextual error messages
❌ Catching everything: Hides real issues
✅ Let dev errors surface in development
When to Use Error Boundaries
✅ Use error boundaries for:
- Isolating features/sections
- Third-party components
- Complex data visualization
- User-generated content rendering
- API-dependent components
❌ Don't use for:
- Event handlers (use try-catch)
- Async operations (use error states)
- Every single component (overkill)
- Server components (use error.tsx)
Error boundaries are your safety net—place them strategically to catch failures without degrading the entire user experience.