
Error boundaries are a powerful React feature that allows you to catch JavaScript errors anywhere in your component tree, log those errors, and display a fallback UI instead of the component tree that crashed. They act as a safety net for your React applications, providing graceful error handling and improving user experience when unexpected errors occur.
Understanding Error Boundaries
Error boundaries are React components that catch JavaScript errors during rendering, in lifecycle methods, and in constructors of the whole tree below them. Think of them as try-catch blocks for React components, but they work declaratively and can be positioned strategically throughout your component tree.
What Error Boundaries Catch
Error boundaries catch errors in:
- Rendering phase
- Lifecycle methods
- Constructors of child components
- During the creation of the component tree
What Error Boundaries Don’t Catch
Error boundaries do not catch errors in:
- Event handlers
- Asynchronous code (setTimeout, requestAnimationFrame callbacks)
- Server-side rendering
- Errors thrown in the error boundary itself
Basic Error Boundary Implementation
Here’s a basic error boundary implementation using class components:
import React from 'react';
class BasicErrorBoundary extends React.Component {
 constructor(props) {
  super(props);
  this.state = { hasError: false };
 }
 static getDerivedStateFromError(error) {
  // Update state so the next render will show the fallback UI
  return { hasError: true };
 }
 componentDidCatch(error, errorInfo) {
  // Log the error to an error reporting service
  console.error('Error caught by boundary:', error, errorInfo);
 }
 render() {
  if (this.state.hasError) {
   // Fallback UI
   return <h1>Something went wrong.</h1>;
  }
  return this.props.children;
 }
}
export default BasicErrorBoundary;
Usage Example
import React from 'react';
import BasicErrorBoundary from './BasicErrorBoundary';
import MyComponent from './MyComponent';
const App = () => {
 return (
  <div>
   <h1>My Application</h1>
   <BasicErrorBoundary>
    <MyComponent />
   </BasicErrorBoundary>
  </div>
 );
};
export default App;
Advanced Error Boundary Implementation
Let’s create a more sophisticated error boundary with enhanced features:
import React from 'react';
import PropTypes from 'prop-types';
class AdvancedErrorBoundary extends React.Component {
 constructor(props) {
  super(props);
  this.state = {
   hasError: false,
   error: null,
   errorInfo: null,
   errorId: null
  };
 }
 static getDerivedStateFromError(error) {
  // Generate unique error ID for tracking
  const errorId = Date.now().toString(36) + Math.random().toString(36).substr(2);
  return {
   hasError: true,
   errorId
  };
 }
 componentDidCatch(error, errorInfo) {
  // Capture error details
  this.setState({
   error,
   errorInfo
  });
  // Log error with additional context
  this.logError(error, errorInfo);
  // Call custom error handler if provided
  if (this.props.onError) {
   this.props.onError(error, errorInfo);
  }
 }
 logError = (error, errorInfo) => {
  const errorData = {
   error: error.toString(),
   errorInfo: errorInfo.componentStack,
   errorId: this.state.errorId,
   timestamp: new Date().toISOString(),
   userAgent: navigator.userAgent,
   url: window.location.href,
   userId: this.getUserId(),
   ...this.props.errorMetadata
  };
  // Log to console in development
  if (process.env.NODE_ENV === 'development') {
   console.group('🚨 Error Boundary Caught Error');
   console.error('Error:', error);
   console.error('Error Info:', errorInfo);
   console.error('Error Data:', errorData);
   console.groupEnd();
  }
  // Send to error reporting service
  this.sendErrorReport(errorData);
 };
 sendErrorReport = async (errorData) => {
  try {
   if (this.props.errorReportingEndpoint) {
    await fetch(this.props.errorReportingEndpoint, {
     method: 'POST',
     headers: {
      'Content-Type': 'application/json',
     },
     body: JSON.stringify(errorData),
    });
 }
  } catch (reportingError) {
   console.error('Failed to send error report:', reportingError);
  }
 };
 getUserId = () => {
  // Implementation to get current user ID
  return localStorage.getItem('userId') || 'anonymous';
 };
 handleRetry = () => {
  this.setState({
   hasError: false,
   error: null,
   errorInfo: null,
   errorId: null
  });
 };
 render() {
  if (this.state.hasError) {
   // Custom fallback UI or default
   if (this.props.fallback) {
    return this.props.fallback(
     this.state.error,
     this.state.errorInfo,
     this.handleRetry
    );
   }
   // Default fallback UI
   return (
    <div className="error-boundary">
     <div className="error-boundary-content">
      <h2>Oops! Something went wrong</h2>
      <p>We're sorry for the inconvenience. The error has been logged and will be investigated.</p>
     Â
      {process.env.NODE_ENV === 'development' && (
       <details className="error-details">
        <summary>Error Details (Development Mode)</summary>
        <pre>{this.state.error && this.state.error.toString()}</pre>
        <pre>{this.state.errorInfo.componentStack}</pre>
       </details>
      )}
     Â
      <div className="error-actions">
       <button onClick={this.handleRetry} className="retry-button">
        Try Again
       </button>
       <button
        onClick={() => window.location.reload()}
        className="reload-button"
       >
        Reload Page
       </button>
      </div>
     Â
      {this.state.errorId && (
       <p className="error-id">Error ID: {this.state.errorId}</p>
      )}
     </div>
    </div>
   );
  }
  return this.props.children;
 }
}
AdvancedErrorBoundary.propTypes = {
 children: PropTypes.node.isRequired,
 fallback: PropTypes.func,
 onError: PropTypes.func,
 errorReportingEndpoint: PropTypes.string,
 errorMetadata: PropTypes.object
};
AdvancedErrorBoundary.defaultProps = {
 errorMetadata: {}
};
export default AdvancedErrorBoundary;
Error Boundary with Hooks (Using react-error-boundary)
While error boundaries must be class components, you can create hook-based utilities to work with them:
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// Custom error fallback component
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>
 );
}
// Custom hook for error handling
function useErrorHandler() {
 return (error, errorInfo) => {
  console.error('Error caught:', error, errorInfo);
  // Send to error reporting service
  // logErrorToService(error, errorInfo);
 };
}
// Component using the hook-based error boundary
function MyApp() {
 const handleError = useErrorHandler();
 return (
  <ErrorBoundary
   FallbackComponent={ErrorFallback}
   onError={handleError}
   onReset={() => window.location.reload()}
  >
   <MyComponent />
  </ErrorBoundary>
 );
}
export default MyApp;
Granular Error Boundaries
Implement error boundaries at different levels of your application for better error isolation:
import React from 'react';
// Route-level error boundary
export const RouteErrorBoundary = ({ children }) => (
 <AdvancedErrorBoundary
  fallback={(error, errorInfo, retry) => (
   <div className="route-error">
    <h1>Page Error</h1>
    <p>This page encountered an error and could not be displayed.</p>
    <button onClick={retry}>Retry</button>
    <button onClick={() => window.history.back()}>Go Back</button>
   </div>
  )}
  errorMetadata={{ level: 'route' }}
 >
  {children}
 </AdvancedErrorBoundary>
);
// Feature-level error boundary
export const FeatureErrorBoundary = ({ children, featureName }) => (
 <AdvancedErrorBoundary
  fallback={(error, errorInfo, retry) => (
   <div className="feature-error">
    <h3>Feature Unavailable</h3>
    <p>The {featureName} feature is temporarily unavailable.</p>
    <button onClick={retry}>Retry</button>
   </div>
  )}
  errorMetadata={{ level: 'feature', featureName }}
 >
  {children}
 </AdvancedErrorBoundary>
);
// Component-level error boundary
export const ComponentErrorBoundary = ({ children, componentName }) => (
 <AdvancedErrorBoundary
  fallback={(error, errorInfo, retry) => (
   <div className="component-error">
    <p>Component Error: {componentName}</p>
    <button onClick={retry}>Retry</button>
   </div>
  )}
  errorMetadata={{ level: 'component', componentName }}
 >
  {children}
 </AdvancedErrorBoundary>
);
// Usage in application
const App = () => {
 return (
  <RouteErrorBoundary>
   <Header />
   <main>
    <FeatureErrorBoundary featureName="User Dashboard">
     <UserDashboard />
    </FeatureErrorBoundary>
   Â
    <FeatureErrorBoundary featureName="Product Catalog">
     <ProductCatalog />
    </FeatureErrorBoundary>
   Â
    <ComponentErrorBoundary componentName="Weather Widget">
     <WeatherWidget />
    </ComponentErrorBoundary>
   </main>
   <Footer />
  </RouteErrorBoundary>
 );
};
Error Boundary for Async Operations
Handle errors in async operations using a custom hook pattern:
import React, { useState, useEffect } from 'react';
// Custom hook for async error handling
export const useAsyncError = () => {
 const [error, setError] = useState(null);
Â
 const throwError = (error) => {
  setError(error);
 };
Â
 useEffect(() => {
  if (error) {
   throw error;
  }
 }, [error]);
Â
 return throwError;
};
// Component using async error handling
const AsyncComponent = () => {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const throwError = useAsyncError();
 useEffect(() => {
  const fetchData = async () => {
   try {
    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 (error) {
    throwError(error);
   } finally {
    setLoading(false);
   }
  };
  fetchData();
 }, [throwError]);
 if (loading) return <div>Loading...</div>;
 return <div>{JSON.stringify(data)}</div>;
};
// Usage with error boundary
const App = () => {
 return (
  <AdvancedErrorBoundary>
   <AsyncComponent />
  </AdvancedErrorBoundary>
 );
};
Error Boundary Testing
Test your error boundaries to ensure they work correctly:
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import AdvancedErrorBoundary from './AdvancedErrorBoundary';
// Component that throws an error
const ThrowingComponent = ({ shouldThrow }) => {
 if (shouldThrow) {
  throw new Error('Test error');
 }
 return <div>No error</div>;
};
describe('AdvancedErrorBoundary', () => {
 // Suppress console.error for cleaner test output
 beforeEach(() => {
  jest.spyOn(console, 'error').mockImplementation(() => {});
 });
 afterEach(() => {
  console.error.mockRestore();
 });
 test('renders children when there is no error', () => {
  render(
   <AdvancedErrorBoundary>
    <ThrowingComponent shouldThrow={false} />
   </AdvancedErrorBoundary>
  );
  expect(screen.getByText('No error')).toBeInTheDocument();
 });
 test('renders error UI when there is an error', () => {
  render(
   <AdvancedErrorBoundary>
    <ThrowingComponent shouldThrow={true} />
   </AdvancedErrorBoundary>
  );
Error Boundary with Context
Create an error boundary that integrates with React Context for global error handling:
import React, { createContext, useContext, useReducer } from 'react';
// Error context
const ErrorContext = createContext();
// Error actions
const ERROR_ACTIONS = {
 ADD_ERROR: 'ADD_ERROR',
 REMOVE_ERROR: 'REMOVE_ERROR',
 CLEAR_ERRORS: 'CLEAR_ERRORS'
};
// Error reducer
const errorReducer = (state, action) => {
 switch (action.type) {
  case ERROR_ACTIONS.ADD_ERROR:
   return {
    ...state,
    errors: [...state.errors, { ...action.payload, id: Date.now() }]
   };
  case ERROR_ACTIONS.REMOVE_ERROR:
   return {
    ...state,
    errors: state.errors.filter(error => error.id !== action.payload)
   };
  case ERROR_ACTIONS.CLEAR_ERRORS:
   return {
    ...state,
    errors: []
   };
  default:
   return state;
 }
};
// Error provider component
export const ErrorProvider = ({ children }) => {
 const [state, dispatch] = useReducer(errorReducer, { errors: [] });
 const addError = (error, component) => {
  dispatch({
   type: ERROR_ACTIONS.ADD_ERROR,
   payload: {
    error: error.toString(),
    component,
    timestamp: new Date().toISOString()
   }
  });
 };
 const removeError = (id) => {
  dispatch({
   type: ERROR_ACTIONS.REMOVE_ERROR,
   payload: id
  });
 };
 const clearErrors = () => {
  dispatch({
   type: ERROR_ACTIONS.CLEAR_ERRORS
  });
 };
 return (
  <ErrorContext.Provider value={{
   errors: state.errors,
   addError,
   removeError,
   clearErrors
  }}>
   {children}
  </ErrorContext.Provider>
 );
};
// Hook to use error context
export const useErrorContext = () => {
 const context = useContext(ErrorContext);
 if (!context) {
  throw new Error('useErrorContext must be used within an ErrorProvider');
 }
 return context;
};
// Context-aware error boundary
export class ContextErrorBoundary extends React.Component {
 constructor(props) {
  super(props);
  this.state = { hasError: false };
 }
 static getDerivedStateFromError(error) {
  return { hasError: true };
 }
 componentDidCatch(error, errorInfo) {
  // Add error to context
  if (this.props.onError) {
   this.props.onError(error, errorInfo);
  }
 }
 render() {
  if (this.state.hasError) {
   return this.props.fallback || <h1>Something went wrong.</h1>;
  }
  return this.props.children;
 }
}
// Error display component
const ErrorDisplay = () => {
 const { errors, removeError, clearErrors } = useErrorContext();
 if (errors.length === 0) return null;
 return (
  <div className="error-display">
   <div className="error-header">
    <h3>Application Errors ({errors.length})</h3>
    <button onClick={clearErrors}>Clear All</button>
   </div>
   {errors.map(error => (
    <div key={error.id} className="error-item">
     <p>{error.error}</p>
     <small>{error.component} - {error.timestamp}</small>
     <button onClick={() => removeError(error.id)}>×</button>
    </div>
   ))}
  </div>
 );
};
Error Boundary Best Practices
1. Strategic Placement
Place error boundaries at strategic locations in your component tree:
const App = () => {
return (
  <ErrorProvider>
   {/* Global error boundary */}
   <AdvancedErrorBoundary>
    <Router>
     {/* Route-level error boundaries */}
     <Routes>
      <Route path="/dashboard" element={
       <RouteErrorBoundary>
        <Dashboard />
       </RouteErrorBoundary>
      } />
      <Route path="/profile" element={
       <RouteErrorBoundary>
        <Profile />
       </RouteErrorBoundary>
      } />
     </Routes>
    </Router>
    {/* Global error display */}
    <ErrorDisplay />
   </AdvancedErrorBoundary>
  </ErrorProvider>
 );
};
2. Error Categorization
Categorize errors for better handling:
const ERROR_TYPES = {
 NETWORK: 'network',
 VALIDATION: 'validation',
 RUNTIME: 'runtime',
 PERMISSION: 'permission'
};
const categorizeError = (error) => {
 if (error.message.includes('fetch')) {
  return ERROR_TYPES.NETWORK;
 }
 if (error.message.includes('validation')) {
  return ERROR_TYPES.VALIDATION;
 }
 if (error.message.includes('permission')) {
  return ERROR_TYPES.PERMISSION;
 }
 return ERROR_TYPES.RUNTIME;
};
// Enhanced error boundary with categorization
class CategorizedErrorBoundary extends React.Component {
 componentDidCatch(error, errorInfo) {
  const category = categorizeError(error);
 Â
  // Handle different error types differently
  switch (category) {
   case ERROR_TYPES.NETWORK:
    this.handleNetworkError(error, errorInfo);
    break;
   case ERROR_TYPES.VALIDATION:
    this.handleValidationError(error, errorInfo);
    break;
   case ERROR_TYPES.PERMISSION:
    this.handlePermissionError(error, errorInfo);
    break;
   default:
    this.handleRuntimeError(error, errorInfo);
  }
 }
 handleNetworkError = (error, errorInfo) => {
  // Specific handling for network errors
  console.log('Network error occurred');
 };
 handleValidationError = (error, errorInfo) => {
  // Specific handling for validation errors
  console.log('Validation error occurred');
 };
 handlePermissionError = (error, errorInfo) => {
  // Specific handling for permission errors
  console.log('Permission error occurred');
 };
 handleRuntimeError = (error, errorInfo) => {
  // General runtime error handling
  console.log('Runtime error occurred');
 };
 // ... rest of the component
}
3. Error Recovery Strategies
Implement different recovery strategies:
const RECOVERY_STRATEGIES = {
 RETRY: 'retry',
 FALLBACK: 'fallback',
 REDIRECT: 'redirect',
 REFRESH: 'refresh'
};
const getRecoveryStrategy = (error, component) => {
 // Determine recovery strategy based on error and component
 if (error.message.includes('network')) {
  return RECOVERY_STRATEGIES.RETRY;
 }
 if (component === 'critical') {
  return RECOVERY_STRATEGIES.REFRESH;
 }
 return RECOVERY_STRATEGIES.FALLBACK;
};
// Error boundary with recovery strategies
class RecoveryErrorBoundary extends React.Component {
 constructor(props) {
  super(props);
  this.state = {
   hasError: false,
   error: null,
   recoveryStrategy: null,
   retryCount: 0
  };
 }
 static getDerivedStateFromError(error) {
  return { hasError: true, error };
 }
 componentDidCatch(error, errorInfo) {
  const strategy = getRecoveryStrategy(error, this.props.component);
  this.setState({ recoveryStrategy: strategy });
 }
 handleRecovery = () => {
  const { recoveryStrategy, retryCount } = this.state;
 Â
  switch (recoveryStrategy) {
   case RECOVERY_STRATEGIES.RETRY:
    if (retryCount < 3) {
     this.setState({
      hasError: false,
      error: null,
      retryCount: retryCount + 1
     });
    } else {
     this.setState({ recoveryStrategy: RECOVERY_STRATEGIES.FALLBACK });
    }
    break;
   case RECOVERY_STRATEGIES.REFRESH:
    window.location.reload();
    break;
   case RECOVERY_STRATEGIES.REDIRECT:
    window.location.href = '/';
    break;
   default:
    this.setState({
     hasError: false,
     error: null,
     retryCount: 0
    });
  }
 };
 render() {
  if (this.state.hasError) {
   return (
    <div className="recovery-error-boundary">
     <h2>Error Occurred</h2>
     <p>{this.state.error?.message}</p>
     <button onClick={this.handleRecovery}>
      {this.getRecoveryButtonText()}
     </button>
    </div>
   );
  }
  return this.props.children;
 }
 getRecoveryButtonText = () => {
  const { recoveryStrategy, retryCount } = this.state;
 Â
  switch (recoveryStrategy) {
   case RECOVERY_STRATEGIES.RETRY:
    return `Retry (${3 - retryCount} attempts left)`;
   case RECOVERY_STRATEGIES.REFRESH:
    return 'Refresh Page';
   case RECOVERY_STRATEGIES.REDIRECT:
    return 'Go to Home';
   default:
    return 'Try Again';
  }
 };
}
Conclusion
Error boundaries are essential for building robust React applications. They provide a declarative way to handle errors and maintain a good user experience even when things go wrong. Key takeaways include:
- Implement error boundaries at strategic locations in your component tree
- Use advanced error boundaries with comprehensive error logging and reporting
- Combine error boundaries with context for global error management
- Implement different recovery strategies based on error types
- Test your error boundaries thoroughly
- Consider user experience when designing fallback UIs
- Use error boundaries to isolate failures and prevent cascading errors
By implementing comprehensive error boundaries, you can create more resilient React applications that gracefully handle unexpected errors and provide meaningful feedback to users.
Key Takeaways
- Error boundaries catch JavaScript errors during rendering, lifecycle methods, and constructors
- They don’t catch errors in event handlers, async code, or server-side rendering
- Place error boundaries strategically to isolate different parts of your application
- Implement comprehensive error logging and reporting for production applications
- Use different recovery strategies based on error types and component criticality
- Test error boundaries to ensure they work correctly in various scenarios
- Consider user experience when designing fallback UIs
In the next chapter, we’ll explore Accessibility in React, learning how to build inclusive applications that work for all users.