ReactError Boundaries

Error Boundaries

By default, if a component throws an error during rendering, the entire React tree unmounts and the user sees a blank page. Error boundaries are React components that catch JavaScript errors anywhere in their child component tree and display a fallback UI instead of crashing the whole application.

Only Class Components Can Be Error Boundaries

This is one of the few remaining reasons to write a class component in modern React. Error boundaries require two lifecycle methods that have no function-component equivalent (as of React 18):

  • static getDerivedStateFromError(error) — called during the render phase when a child throws; return new state to trigger the fallback UI

  • componentDidCatch(error, info) — called after the render phase; used for logging errors to an error reporting service (Sentry, Datadog, etc.)

Complete Error Boundary Implementation

JSX
import React from 'react'

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

  // Invoked during render when a descendant throws
  // Return value merges into state
  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  // Invoked after render — ideal for logging
  componentDidCatch(error, errorInfo) {
    // errorInfo.componentStack is the React component stack trace
    console.error('Uncaught error:', error, errorInfo)

    // Send to an error-tracking service:
    // reportError({ error, componentStack: errorInfo.componentStack })
  }

  render() {
    if (this.state.hasError) {
      // Render fallback UI — can be a custom prop or a default
      if (this.props.fallback) {
        return this.props.fallback
      }

      return (
        <div role="alert" style={{ padding: '1rem', border: '1px solid red' }}>
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error?.message}
          </details>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try again
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

export default ErrorBoundary
Placement Strategy

Where you place an error boundary determines how much of the UI survives an error. Wrapping the entire app in one boundary is the minimum — but it means a single broken widget takes down everything. Granular boundaries give users a better experience.

JSX
// ✗ Coarse — any error kills the whole page
<ErrorBoundary>
  <App />
</ErrorBoundary>

// ✓ Granular — each section degrades independently
function Dashboard() {
  return (
    <div>
      {/* Sidebar error won't affect the main feed */}
      <ErrorBoundary fallback={<p>Sidebar unavailable.</p>}>
        <Sidebar />
      </ErrorBoundary>

      {/* Each widget is isolated */}
      <ErrorBoundary fallback={<WidgetError name="Analytics" />}>
        <AnalyticsWidget />
      </ErrorBoundary>

      <ErrorBoundary fallback={<WidgetError name="Activity Feed" />}>
        <ActivityFeed />
      </ErrorBoundary>
    </div>
  )
}
Note
A good rule of thumb: place an error boundary around every independently-usable section. Route-level boundaries are a minimum; widget-level boundaries are ideal for dashboards and complex UIs.
The react-error-boundary Library

Writing a class component every time is tedious. The react-error-boundary package wraps the complexity in a clean functional API:

Bash
npm install react-error-boundary

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

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) => console.error(error, info)}
      onReset={() => {
        // Optional: reset app state before re-rendering children
        queryClient.clear()
      }}
    >
      <ComplexFeature />
    </ErrorBoundary>
  )
}
useErrorBoundary Hook (React 19+)

React 19 introduces useErrorBoundary, which lets function components imperatively throw errors into the nearest error boundary — useful for async operations that can't be caught by the boundary's render-phase detection:

JSX
import { useErrorBoundary } from 'react'

function UserProfile({ userId }) {
  const { showBoundary } = useErrorBoundary()

  async function loadUser() {
    try {
      const data = await fetchUser(userId)
      setUser(data)
    } catch (err) {
      // Throw the async error into the nearest error boundary
      showBoundary(err)
    }
  }

  // ...
}

// react-error-boundary also provides this hook for React 18:
import { useErrorBoundary } from 'react-error-boundary'
What Error Boundaries Do NOT Catch
Warning
Error boundaries only catch errors that occur during rendering, in lifecycle methods, and in constructors of class components. They do NOT catch: errors inside event handlers (use try/catch there), errors in asynchronous code (setTimeout, fetch callbacks), errors in the error boundary itself, or errors thrown on the server (during SSR).

JSX
// ✗ Error boundary will NOT catch this
function Button() {
  function handleClick() {
    throw new Error('Event handler error') // NOT caught by boundary
  }
  return <button onClick={handleClick}>Click me</button>
}

// ✓ Use try/catch for event handler errors
function Button() {
  function handleClick() {
    try {
      doSomethingRisky()
    } catch (err) {
      setError(err.message) // handle locally
    }
  }
  return <button onClick={handleClick}>Click me</button>
}

// ✗ Error boundary will NOT catch async errors either
function DataFetcher() {
  React.useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(data => setData(data))
      .catch(err => {
        // This catch is needed — boundary won't help here
        setError(err)
      })
  }, [])
}
Tip
Combine error boundaries with React Query or SWR for async data errors. These libraries catch fetch errors and expose them as state (`isError`, `error`) that you can handle in your component's render. Reserve error boundaries for truly unexpected rendering errors.