Error Boundaries: Graceful Failure in React

As your application grows, more components are added and it becomes harder to track all possible exceptions during rendering. React offers Error Boundaries — special components that catch errors and display fallback UI instead of crashing your entire app.

flowchart TD
    subgraph WithBoundary["With Error Boundary ✅"]
        A1["App"] --> B1["ErrorBoundary"]
        B1 --> C1["Widget"]
        C1 -->|"💥 Crash"| D1["Fallback UI"]
        B1 --> E1["Rest of App ✓"]
    end
    
    subgraph WithoutBoundary["Without Error Boundary ❌"]
        A2["App"] --> C2["Widget"]
        C2 -->|"💥 Crash"| D2["Entire App Crashes"]
    end
    
    style D1 fill:#fbbf24
    style D2 fill:#ef4444
    style E1 fill:#10b981


What Does an Error Boundary Catch?

Caught ✅ Not Caught ❌
Errors during rendering Event handlers
Lifecycle method errors Async code (setTimeout, fetch)
Constructor errors Server-side rendering
Errors in child components Errors in the boundary itself
ImportantEvent Handlers Need Try-Catch

Error boundaries only catch errors during the render phase. For event handlers, you still need traditional try-catch blocks.


Where to Place Error Boundaries

This is the most common question. Here’s the strategic placement pattern:

flowchart TD
    App["🏠 App"]
    
    App --> Layout["Layout"]
    
    Layout --> Sidebar["📁 Sidebar"]
    Layout --> Main["📄 Main Content"]
    
    Main --> Route1["Route: Dashboard"]
    Main --> Route2["Route: Settings"]
    
    Route1 --> Widget1["📊 Chart Widget"]
    Route1 --> Widget2["📈 Analytics Widget"]
    Route1 --> Widget3["🔔 Notifications"]
    
    subgraph Boundaries["Error Boundary Placement"]
        B1["🛡️ Route-Level<br/>(catches page crashes)"]
        B2["🛡️ Widget-Level<br/>(isolates failures)"]
        B3["🛡️ Third-Party<br/>(untrusted code)"]
    end
    
    style App fill:#3b82f6
    style B1 fill:#fbbf24
    style B2 fill:#fbbf24
    style B3 fill:#fbbf24

Placement Strategy

Level When to Use Example
App Root Last resort fallback “Something went wrong. Reload?”
Route/Page Each route gets its own boundary Dashboard, Settings, Profile
Feature/Widget Independent features that can fail Charts, Comments, Chat
Third-Party External libraries you don’t control Embeds, Plugins, SDKs

The Modern Way: react-error-boundary

The react-error-boundary library provides a clean, hook-friendly API:

npm install react-error-boundary

Basic Usage

import { ErrorBoundary } from 'react-error-boundary';

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>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, info) => {
        // Log to Sentry, LogRocket, etc.
        console.error('Error caught:', error, info);
      }}
      onReset={() => {
        // Reset app state if needed
        window.location.reload();
      }}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}

Reset on Navigation

import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      resetKeys={[location.pathname]}  // Reset when route changes
    >
      <Routes />
    </ErrorBoundary>
  );
}

useErrorBoundary Hook

For programmatic error throwing:

import { useErrorBoundary } from 'react-error-boundary';

function DataFetcher() {
  const { showBoundary } = useErrorBoundary();

  const fetchData = async () => {
    try {
      const res = await fetch('/api/data');
      if (!res.ok) throw new Error('Failed to fetch');
      return res.json();
    } catch (error) {
      showBoundary(error);  // Trigger the error boundary
    }
  };

  // ...
}

Component Hierarchy Example

Here’s a real-world component tree with strategic error boundary placement:

// App.tsx
import { ErrorBoundary } from 'react-error-boundary';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      {/* 🛡️ Level 1: App-wide crash protection */}
      <ErrorBoundary FallbackComponent={AppCrashFallback}>
        <Layout>
          <Sidebar />
          
          <main>
            {/* 🛡️ Level 2: Route-level isolation */}
            <ErrorBoundary FallbackComponent={PageErrorFallback}>
              <Routes>
                <Route path="/" element={<Dashboard />} />
                <Route path="/settings" element={<Settings />} />
              </Routes>
            </ErrorBoundary>
          </main>
        </Layout>
      </ErrorBoundary>
    </BrowserRouter>
  );
}

// Dashboard.tsx
function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>
      
      <div className="widgets">
        {/* 🛡️ Level 3: Widget-level isolation */}
        <ErrorBoundary FallbackComponent={WidgetErrorFallback}>
          <AnalyticsChart />
        </ErrorBoundary>
        
        <ErrorBoundary FallbackComponent={WidgetErrorFallback}>
          <RecentActivity />
        </ErrorBoundary>
        
        {/* 🛡️ Level 4: Third-party isolation */}
        <ErrorBoundary FallbackComponent={WidgetErrorFallback}>
          <ThirdPartyEmbed />
        </ErrorBoundary>
      </div>
    </div>
  );
}

Fallback UI Patterns

Minimal Widget Fallback

function WidgetErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div className="widget-error">
      <span>⚠️ Failed to load</span>
      <button onClick={resetErrorBoundary}>Retry</button>
    </div>
  );
}

Informative Page Fallback

function PageErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div className="page-error">
      <h2>This page encountered an error</h2>
      <p>We've been notified and are working on a fix.</p>
      
      <details>
        <summary>Error details</summary>
        <pre>{error.message}</pre>
        <pre>{error.stack}</pre>
      </details>
      
      <div className="actions">
        <button onClick={resetErrorBoundary}>Try again</button>
        <button onClick={() => window.location.href = '/'}>
          Go to homepage
        </button>
      </div>
    </div>
  );
}

Full App Crash Fallback

function AppCrashFallback({ error }) {
  return (
    <div className="app-crash">
      <h1>😵 Something went wrong</h1>
      <p>The application has crashed. Please reload the page.</p>
      <button onClick={() => window.location.reload()}>
        Reload Application
      </button>
    </div>
  );
}

Production Error Logging

Integrate with error tracking services:

import * as Sentry from '@sentry/react';
import { ErrorBoundary } from 'react-error-boundary';

function logError(error: Error, info: { componentStack: string }) {
  Sentry.captureException(error, {
    extra: {
      componentStack: info.componentStack,
    },
  });
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={logError}
    >
      <Routes />
    </ErrorBoundary>
  );
}

The Manual Way: Class Component

If you need zero dependencies or want to understand how it works:

import React, { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

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) {
    console.error('Error caught by boundary:', error, errorInfo);
    this.props.onError?.(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div>Something went wrong.</div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Best Practices

TipError Boundary Strategy
  1. Don’t wrap everything — Be strategic, not paranoid
  2. Isolate untrusted code — Third-party libraries get their own boundary
  3. Reset on navigation — Use resetKeys with route changes
  4. Log to a service — Sentry, LogRocket, or your own backend
  5. Keep fallbacks helpful — Show actionable UI, not just “Error”
  6. Test your boundaries — Intentionally throw errors in development
Do ✅ Don’t ❌
Wrap routes independently Wrap entire app in one boundary
Isolate widgets Let one widget crash the page
Log errors to a service Silently swallow errors
Provide retry buttons Show only “Something went wrong”
Reset on navigation Keep error state forever

Testing Error Boundaries

// ErrorBoundary.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ErrorBoundary } from 'react-error-boundary';

function ThrowError() {
  throw new Error('Test error');
}

test('renders fallback when child throws', () => {
  render(
    <ErrorBoundary fallback={<div>Error occurred</div>}>
      <ThrowError />
    </ErrorBoundary>
  );

  expect(screen.getByText('Error occurred')).toBeInTheDocument();
});

test('resets when resetErrorBoundary is called', async () => {
  const user = userEvent.setup();
  let shouldThrow = true;

  function MaybeThrow() {
    if (shouldThrow) throw new Error('Test error');
    return <div>Success</div>;
  }

  render(
    <ErrorBoundary
      FallbackComponent={({ resetErrorBoundary }) => (
        <button onClick={resetErrorBoundary}>Reset</button>
      )}
      onReset={() => { shouldThrow = false; }}
    >
      <MaybeThrow />
    </ErrorBoundary>
  );

  await user.click(screen.getByText('Reset'));
  expect(screen.getByText('Success')).toBeInTheDocument();
});

Error boundaries transform your app from “one crash kills everything” to “graceful degradation with user feedback.” Place them strategically, log errors properly, and always give users a way to recover.