React Error Boundaries: Graceful Error Handling (2026)

Without error boundaries, an unhandled JavaScript error inside a React component tree unmounts the entire app and shows a blank screen. Error boundaries catch rendering errors in their subtree, log them, and display a fallback UI — keeping the rest of the app functional. This guide covers everything from class-based boundaries to the modern react-error-boundary library, reset strategies and async error handling.

What Error Boundaries Catch

Error boundaries catch errors during:

  • Rendering (including render methods)
  • Lifecycle methods
  • Constructors of child components

They do not catch:

  • Event handlers (use try/catch)
  • Async code (setTimeout, fetch callbacks) — unless you rethrow into React's state
  • Server-side rendering errors
  • Errors in the boundary itself
Development vs Production: In development, React re-throws caught errors after calling componentDidCatch, so you'll still see the full error overlay. In production, the fallback UI renders. Test your boundaries with a production build.

Class-Based Error Boundaries

Error boundaries must be class components — they're one of the few remaining reasons to write class components:

import { Component } from 'react'

class ErrorBoundary extends Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error) {
    // Called during rendering — update state to show fallback
    return { hasError: true, error }
  }

  componentDidCatch(error, info) {
    // Called after render — good for logging
    console.error('Error caught by boundary:', error, info.componentStack)
    // reportToSentry(error, info)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <h2>Something went wrong.</h2>
    }
    return this.props.children
  }
}

// Usage
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

react-error-boundary Library

The react-error-boundary package provides a flexible, function-friendly API:

npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'

// Simple fallback component
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{ color: 'red' }}>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, info) => logError(error, info)}
      onReset={() => {
        // Reset app state here
      }}
    >
      <UserProfile />
    </ErrorBoundary>
  )
}

Inline fallback with fallbackRender prop:

<ErrorBoundary
  fallbackRender={({ error, resetErrorBoundary }) => (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={resetErrorBoundary}>Retry</button>
    </div>
  )}
>
  <DataWidget />
</ErrorBoundary>

Reset and Recovery Strategies

Use resetKeys to automatically reset the boundary when certain values change:

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

function ProductPage({ productId }) {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      // Reset automatically when productId changes (user navigates to different product)
      resetKeys={[productId]}
    >
      <ProductDetails id={productId} />
    </ErrorBoundary>
  )
}

Programmatic reset with useErrorBoundary:

'use client'
import { useErrorBoundary } from 'react-error-boundary'

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

  async function loadData() {
    try {
      const data = await fetchData()
      setData(data)
    } catch (error) {
      // Propagate async error to nearest ErrorBoundary
      showBoundary(error)
    }
  }

  return <button onClick={loadData}>Load Data</button>
}

Async Errors

Errors in event handlers and async functions don't propagate to error boundaries by default. Two approaches:

// Approach 1: useErrorBoundary hook (react-error-boundary)
import { useErrorBoundary } from 'react-error-boundary'

function AsyncComponent() {
  const { showBoundary } = useErrorBoundary()

  useEffect(() => {
    fetchData()
      .then(setData)
      .catch(showBoundary)  // Routes error to boundary
  }, [])
}

// Approach 2: setState trick (works with any error boundary)
function useAsyncError() {
  const [, setError] = useState()
  return useCallback(
    error => setError(() => { throw error }),
    []
  )
}

function AsyncComponent() {
  const throwError = useAsyncError()

  useEffect(() => {
    fetchData().catch(throwError)
  }, [])
}

Error Boundaries with Suspense

Always pair Suspense with an Error Boundary — a suspended component that rejects its promise needs an error boundary to catch the failure:

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

function SafeDataView() {
  return (
    <ErrorBoundary
      FallbackComponent={({ error, resetErrorBoundary }) => (
        <div>
          <p>Failed to load: {error.message}</p>
          <button onClick={resetErrorBoundary}>Retry</button>
        </div>
      )}
    >
      <Suspense fallback={<Spinner />}>
        <DataView />
      </Suspense>
    </ErrorBoundary>
  )
}

Production Error Reporting

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

function onError(error, info) {
  Sentry.withScope(scope => {
    scope.setExtra('componentStack', info.componentStack)
    Sentry.captureException(error)
  })
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={GlobalErrorFallback}
      onError={onError}
    >
      <Router />
    </ErrorBoundary>
  )
}

Error Boundaries in Next.js

Next.js App Router uses error.tsx files as error boundaries per route segment. These must be Client Components:

// app/dashboard/error.tsx
'use client'

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    console.error(error)
  }, [error])

  return (
    <div>
      <h2>Something went wrong in Dashboard</h2>
      <button onClick={reset}>Try again</button>
    </div>
  )
}

// app/global-error.tsx — catches errors in the root layout
'use client'
export default function GlobalError({ error, reset }) {
  return (
    <html><body>
      <h2>Something went wrong</h2>
      <button onClick={reset}>Try again</button>
    </body></html>
  )
}