Background pattern
New

A Simple Guide to React 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 instead of the crashed component tree.

reacterror-handlingerror-boundariesuijavascript

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 the crashed component tree. They catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

NOTE: Error boundaries only catch errors in the components below them in the tree. An error boundary can't catch an error within itself. This post assumes you have a basic understanding of React and JavaScript.

Important Note on Component Types: Error boundaries in React versions prior to React 19 require class components because they need access to specific lifecycle methods (static getDerivedStateFromError and componentDidCatch) that aren't available in functional components. In React 19+, you can use functional components with the onError prop. This guide covers both approaches.

## What Errors Do Error Boundaries Catch?

Error boundaries catch JavaScript errors that occur:

  1. During rendering
  2. In lifecycle methods
  3. In constructors
  4. In event handlers (with a different approach)

## What Errors Don't They Catch?

Error boundaries do not catch errors for:

  1. Event handlers - Use regular try/catch instead
  2. Asynchronous code (e.g., setTimeout, promise callbacks)
  3. Server-side rendering
  4. Errors thrown in the error boundary itself

## Creating a Basic Error Boundary

### React 19+ Approach (Functional Components)

Starting with React 19, you can create error boundaries using functional components with the built-in React.ErrorBoundary component:

react-19-error-boundary.tsx
import * as React from 'react';

function ErrorFallback({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div style={{ padding: '20px', border: '1px solid red', borderRadius: '8px' }}>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <React.ErrorBoundary
      fallback={(error, reset) => (
        <ErrorFallback
          error={error}
          reset={reset}
        />
      )}
    >
      <YourComponent />
    </React.ErrorBoundary>
  );
}

### React 18 and Earlier (Class Components)

Note: Error boundaries in React versions prior to React 19 must be class components. This is a React requirement because error boundaries need access to specific lifecycle methods that aren't available in functional components.

Class component error boundaries implement either or both of these lifecycle methods:

  • static getDerivedStateFromError() - For rendering a fallback UI
  • componentDidCatch() - For logging error information

Here's a basic error boundary for React 18 and earlier:

basic-error-boundary.tsx
import * as React from 'react';

interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

interface ErrorBoundaryProps {
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

/**
 * Error Boundary for React 18 and earlier
 * NOTE: Class components are required for error boundaries in React < 19
 * because they need access to static getDerivedStateFromError and componentDidCatch
 */
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    // Update state so the next render will show the fallback UI
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    // You can also log the error to an error reporting service
    console.error('Error caught by boundary:', error, errorInfo);

    // Example: Log to error reporting service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return (
        this.props.fallback || (
          <div style={{ padding: '20px', border: '1px solid red', borderRadius: '8px' }}>
            <h1>Something went wrong.</h1>
            <p>{this.state.error?.message}</p>
          </div>
        )
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

## Using Error Boundaries

You can wrap any component with an error boundary:

using-error-boundary.tsx
import * as React from 'react';
import ErrorBoundary from './ErrorBoundary';

function BuggyComponent() {
  // This will throw an error
  throw new Error('I crashed!');

  return <div>This will never render</div>;
}

function App() {
  return (
    <ErrorBoundary>
      <BuggyComponent />
    </ErrorBoundary>
  );
}

## Custom Fallback UI

You can provide a custom fallback component:

custom-fallback.tsx
import * as React from 'react';

interface ErrorFallbackProps {
  error: Error;
  resetError?: () => void;
}

function ErrorFallback({ error, resetError }: ErrorFallbackProps) {
  return (
    <div
      role="alert"
      style={{
        padding: '40px',
        maxWidth: '600px',
        margin: '40px auto',
        textAlign: 'center',
        backgroundColor: '#fee',
        border: '1px solid #fcc',
        borderRadius: '8px'
      }}
    >
      <h2 style={{ color: '#c33' }}>Oops! Something went wrong</h2>
      <p style={{ color: '#666' }}>{error.message}</p>
      {resetError && (
        <button
          onClick={resetError}
          style={{
            padding: '10px 20px',
            backgroundColor: '#c33',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          Try Again
        </button>
      )}
    </div>
  );
}

// Enhanced Error Boundary with reset functionality
/**
 * NOTE: Class components are required for error boundaries in React < 19
 * Consider using React 19's React.ErrorBoundary for new projects
 */
interface EnhancedErrorBoundaryProps {
  children: React.ReactNode;
  fallback?: (error: Error, resetError: () => void) => React.ReactNode;
}

interface EnhancedErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

class EnhancedErrorBoundary extends React.Component<EnhancedErrorBoundaryProps, EnhancedErrorBoundaryState> {
  constructor(props: EnhancedErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): EnhancedErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    console.error('Error caught:', error, errorInfo);
  }

  resetError = () => {
    this.setState({ hasError: false, error: undefined });
  };

  render() {
    if (this.state.hasError && this.state.error) {
      if (this.props.fallback) {
        return this.props.fallback(this.state.error, this.resetError);
      }
      return (
        <ErrorFallback
          error={this.state.error}
          resetError={this.resetError}
        />
      );
    }

    return this.props.children;
  }
}

## Functional Error Boundary with Hooks

Important: Prior to React 19, you cannot create a true error boundary using only hooks and functional components. Error boundaries require access to static getDerivedStateFromError and componentDidCatch lifecycle methods, which are only available in class components.

However, you can create a simplified error handling component using hooks for specific use cases like handling manually thrown errors:

functional-error-handler.tsx
import * as React from 'react';

interface ErrorHandlerProps {
  children: React.ReactNode;
  fallback: (error: Error, reset: () => void) => React.ReactNode;
}

interface ErrorHandlerState {
  error: Error | null;
}

/**
 * NOTE: This is NOT a true error boundary and won't catch rendering errors.
 * Use class components or React 19's React.ErrorBoundary for actual error boundaries.
 * This component only handles manually thrown errors via the throwError prop.
 */
export function ErrorHandler({ children, fallback }: ErrorHandlerProps) {
  const [error, setError] = React.useState<Error | null>(null);

  // This is a simplified version for demonstration
  // It cannot catch rendering errors like a true error boundary
  if (error) {
    return fallback(error, () => setError(null));
  }

  return <>{children}</>;
}

Recommendation: For React 18 and earlier, use class components for error boundaries. For React 19+, use the built-in React.ErrorBoundary component shown above.

## Handling Errors in Event Handlers

Error boundaries don't catch errors in event handlers. Use try/catch for those:

event-handler-errors.tsx
import * as React from 'react';

function ButtonComponent() {
  const handleClick = () => {
    try {
      // Code that might throw an error
      if (Math.random() > 0.5) {
        throw new Error('Random error occurred!');
      }
      console.log('Success!');
    } catch (error) {
      console.error('Caught in event handler:', error);
      // You can update state to show an error message
    }
  };

  return <button onClick={handleClick}>Click me (might throw an error)</button>;
}

## Handling Asynchronous Errors

For async errors, wrap your async operations:

async-errors.tsx
import * as React from 'react';

function AsyncComponent() {
  const [error, setError] = React.useState<Error | null>(null);
  const [data, setData] = React.useState<string | null>(null);

  const fetchData = async () => {
    try {
      const response = await fetch('/api/data');
      if (!response.ok) {
        throw new Error('Failed to fetch data');
      }
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err as Error);
    }
  };

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      <button onClick={fetchData}>Fetch Data</button>
      {data && <div>{data}</div>}
    </div>
  );
}

## Where to Place Error Boundaries

You can place error boundaries at different levels of your component tree:

error-boundary-placement.tsx
import * as React from 'react';
import ErrorBoundary from './ErrorBoundary';

function Sidebar() {
  return <div>Sidebar Content</div>;
}

function MainContent() {
  // This component might crash
  throw new Error('Main content crashed!');
  return <div>Main Content</div>;
}

function Header() {
  return <div>Header</div>;
}

function App() {
  return (
    <div>
      {/* Header has its own error boundary */}
      <ErrorBoundary fallback={<div>Header Error</div>}>
        <Header />
      </ErrorBoundary>

      <div style={{ display: 'flex' }}>
        {/* Sidebar is protected separately */}
        <ErrorBoundary fallback={<div>Sidebar Error</div>}>
          <Sidebar />
        </ErrorBoundary>

        {/* Main content is protected separately */}
        <ErrorBoundary fallback={<div>Main Content Error</div>}>
          <MainContent />
        </ErrorBoundary>
      </div>
    </div>
  );
}

## Real-World Examples

### Protecting a Data Fetching Component

data-fetching-protection.tsx
import * as React from 'react';
import ErrorBoundary from './ErrorBoundary';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = React.useState<User | null>(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => {
        if (!res.ok) throw new Error('User not found');
        return res.json();
      })
      .then((data) => {
        setUser(data);
        setLoading(false);
      })
      .catch((error) => {
        setLoading(false);
        throw error; // This will be caught by the error boundary
      });
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (!user) return null;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      fallback={(error, reset) => (
        <div>
          <h2>Failed to load user profile</h2>
          <p>{error.message}</p>
          <button onClick={reset}>Retry</button>
        </div>
      )}
    >
      <UserProfile userId={1} />
    </ErrorBoundary>
  );
}

### Protecting a Form Component

form-protection.tsx
import * as React from 'react';
import ErrorBoundary from './ErrorBoundary';

interface FormData {
  name: string;
  email: string;
}

function ComplexForm() {
  const [formData, setFormData] = React.useState<FormData>({
    name: '',
    email: ''
  });

  // Simulating a complex form that might have rendering issues
  const validateEmail = (email: string) => {
    if (!email.includes('@')) {
      throw new Error('Invalid email format');
    }
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    try {
      validateEmail(formData.email);
      // Submit form data
    } catch (error) {
      // Handle error in event handler (won't be caught by boundary)
      alert((error as Error).message);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder="Name"
      />
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder="Email"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

function App() {
  return (
    <ErrorBoundary
      fallback={
        <div>
          <h2>Form Error</h2>
          <p>Please refresh the page and try again.</p>
        </div>
      }
    >
      <ComplexForm />
    </ErrorBoundary>
  );
}

### Protecting Third-Party Components

third-party-protection.tsx
import * as React from 'react';
import UnstableThirdPartyComponent from 'some-library';
import ErrorBoundary from './ErrorBoundary';

function App() {
  return (
    <div>
      <h1>My App</h1>

      {/* Protect the entire app from third-party component errors */}
      <ErrorBoundary
        fallback={
          <div>
            <h2>Third-party component failed</h2>
            <p>The component encountered an error. Please try again later.</p>
          </div>
        }
      >
        <UnstableThirdPartyComponent />
      </ErrorBoundary>
    </div>
  );
}

## Logging Errors to a Service

You can integrate error boundaries with error tracking services:

error-logging.tsx
import * as React from 'react';
import * as Sentry from '@sentry/react';

interface LoggingErrorBoundaryProps {
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

interface LoggingErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

/**
 * Logging Error Boundary for React 18 and earlier
 * NOTE: Class components are required for error boundaries in React < 19
 * For React 19+, consider using React.ErrorBoundary with error reporting
 */
class LoggingErrorBoundary extends React.Component<LoggingErrorBoundaryProps, LoggingErrorBoundaryState> {
  constructor(props: LoggingErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): LoggingErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    // Log to Sentry
    Sentry.captureException(error, {
      contexts: {
        react: {
          componentStack: errorInfo.componentStack
        }
      }
    });

    // Or log to your own service
    // logErrorToService({
    //   error: error.toString(),
    //   errorInfo: errorInfo.componentStack,
    //   timestamp: new Date().toISOString()
    // });
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div>
            <h1>Something went wrong.</h1>
            <p>The error has been logged. We'll look into it!</p>
          </div>
        )
      );
    }

    return this.props.children;
  }
}

## Best Practices

  1. Place error boundaries strategically - Wrap components that are likely to fail
  2. Don't overuse - Not every component needs its own error boundary
  3. Provide helpful fallback UI - Give users a way to recover or navigate away
  4. Log errors - Always log errors to a service for debugging
  5. Test error boundaries - Make sure they work as expected
  6. Reset state when possible - Allow users to retry after an error

## Conclusion

Error boundaries are an essential tool for building robust React applications. They provide a graceful way to handle errors in your component tree and prevent the entire app from crashing. By strategically placing error boundaries and providing helpful fallback UIs, you can improve user experience and make debugging easier.

### Choosing the Right Approach

For React 19+ projects:

  • Prefer the built-in React.ErrorBoundary with functional components
  • It's modern, easier to use, and aligns with functional component patterns

For React 18 and earlier:

  • Class components are required for error boundaries
  • The examples in this guide provide production-ready implementations
  • Consider upgrading to React 19 to use functional component error boundaries

Remember:

  • Error boundaries catch errors in rendering, lifecycle methods, and constructors
  • They don't catch errors in event handlers or async code (use try/catch for those)
  • You can use them at any level in your component tree
  • Always log errors to help with debugging
  • Class components are only required for error boundaries in React < 19, not for other use cases

Published on January 26, 2026

11 min read

Found an Issue!

Find an issue with this post? Think you could clarify, update or add something? All my posts are available to edit on Github. Any fix, little or small, is appreciated!

Edit on GitHub

Last updated on