Background pattern

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 require class components because they rely on lifecycle methods (static getDerivedStateFromError and componentDidCatch) that aren't available in functional components. This is still true in React 19 — React does not ship a built-in error boundary component. If you want a functional-style API, the community-standard react-error-boundary library wraps a class boundary for you. This guide covers writing your own class boundary and using the library.

## 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

### Functional API with react-error-boundary

React has no built-in error boundary component — not in React 18, and not in React 19. The most popular way to get a functional-friendly API is the react-error-boundary library, which provides a ready-made <ErrorBoundary> component you can drop in:

react-error-boundary-lib.tsx
import { ErrorBoundary } from 'react-error-boundary';

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

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <YourComponent />
    </ErrorBoundary>
  );
}

Under the hood react-error-boundary is still a class component — it just hides the boilerplate behind a clean, functional-first API (FallbackComponent, onError, onReset, and a useErrorBoundary hook for throwing async errors).

### React 19 Root-Level Error Handlers

React 19 didn't add an error boundary component, but it did add root-level error callbacks so you can log every error in one place. Pass onCaughtError and onUncaughtError when you create the root:

react-19-root-handlers.tsx
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root')!, {
  // Fired for errors that an error boundary caught
  onCaughtError: (error, errorInfo) => {
    console.error('Caught by a boundary:', error, errorInfo.componentStack);
  },
  // Fired for errors no boundary caught
  onUncaughtError: (error, errorInfo) => {
    console.error('Not caught by any boundary:', error, errorInfo.componentStack);
  }
});

root.render(<App />);

These callbacks complement error boundaries — they don't replace them. You still need a boundary (your own class or react-error-boundary) to render a fallback UI.

### Writing Your Own Class Component Error Boundary

Note: If you'd rather not add a dependency, you write the boundary yourself. It must be a class component — this is a React requirement (still true in React 19) because error boundaries need access to 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';

type  = {
  : boolean;
  ?: Error;
}

type  = {
  : React.;
  ?: React.;
}

/**
 * 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  extends React.<, > {
  constructor(: ) {
    super();
    this. = { : false };
  }

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

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

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

  () {
    if (this..) {
      // You can render any custom fallback UI
      return (
        this.. || (
          < ={{ : '20px', : '1px solid red', : '8px' }}>
            <>Something went wrong.</>
            <>{this..?.}</>
          </>
        )
      );
    }

    return this..;
  }
}

export default ;

## 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';

type  = {
  : Error;
  ?: () => void;
}

function ({ ,  }: ) {
  return (
    <
      ="alert"
      ={{
        : '40px',
        : '600px',
        : '40px auto',
        : 'center',
        : '#fee',
        : '1px solid #fcc',
        : '8px'
      }}
    >
      < ={{ : '#c33' }}>Oops! Something went wrong</>
      < ={{ : '#666' }}>{.}</>
      { && (
        <
          ={}
          ={{
            : '10px 20px',
            : '#c33',
            : 'white',
            : 'none',
            : '4px',
            : 'pointer'
          }}
        >
          Try Again
        </>
      )}
    </>
  );
}

// Enhanced Error Boundary with reset functionality
/**
 * NOTE: Error boundaries must be class components in every React version.
 * Consider the react-error-boundary library if you prefer a functional API.
 */
type  = {
  : React.;
  ?: (: Error, : () => void) => React.;
};

type  = {
  : boolean;
  ?: Error;
}

class  extends React.<, > {
  constructor(: ) {
    super();
    this. = { : false };
  }

  static (: Error):  {
    return { : true,  };
  }

  (: Error, : React.ErrorInfo): void {
    .('Error caught:', , );
  }

   = () => {
    this.({ : false, :  });
  };

  () {
    if (this.. && this..) {
      if (this..) {
        return this..(this.., this.);
      }
      return (
        <
          ={this..}
          ={this.}
        />
      );
    }

    return this..;
  }
}

## Functional Error Boundary with Hooks

Important: You cannot create a true error boundary using only hooks and functional components, in any React version. 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 {
  : React.;
  : (: Error, : () => void) => React.;
}

interface ErrorHandlerState {
  : Error | null;
}

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

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

  return <>{}</>;
}

Recommendation: Write a class component when you want full control, or reach for the react-error-boundary library when you want a functional API with reset support built in. This applies to React 19 as well — there is no built-in error boundary component to fall back on.

## 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 () {
  const  = () => {
    try {
      // Code that might throw an error
      if (.() > 0.5) {
        throw new ('Random error occurred!');
      }
      .('Success!');
    } catch () {
      .('Caught in event handler:', );
      // You can update state to show an error message
    }
  };

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

## Handling Asynchronous Errors

For async errors, wrap your async operations:

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

function () {
  const [, ] = React.<Error | null>(null);
  const [, ] = React.<string | null>(null);

  const  = async () => {
    try {
      const  = await ('/api/data');
      if (!.) {
        throw new ('Failed to fetch data');
      }
      const  = await .();
      ();
    } catch () {
      ( as Error);
    }
  };

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

  return (
    <>
      < ={}>Fetch Data</>
      { && <>{}</>}
    </>
  );
}

## 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
 * NOTE: Error boundaries must be class components in every React version.
 * The react-error-boundary library exposes the same idea via an onError callback.
 */
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

Roll your own class component when:

  • You want zero dependencies and full control over the boundary's behaviour
  • The examples in this guide provide production-ready implementations to start from

Reach for react-error-boundary when:

  • You prefer a functional-first API (FallbackComponent, onError, onReset)
  • You want built-in reset support and a useErrorBoundary hook for async errors

Either way, remember there is no built-in error boundary component in React — including React 19. What React 19 does add is root-level onCaughtError / onUncaughtError callbacks for centralized logging.

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
  • The boundary itself must be a class component (or a library that wraps one) in every React version

Published on January 26, 2026

13 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