A component that reads a prop it was never given will take down the whole tree in production. React renders nothing but the white screen of sadness; in development, the create-react-app overlay points at the offender. The fix in the example is to pass subject as a string or default its value, but runtime errors of this shape are common enough to deserve a general handling strategy.

Why try/catch Doesn't Save You

Wrapping the render in a try/catch appears to work — the error is caught and a fallback message renders — but it only helps if you are willing to wrap every component. Lifting the block to the calling function, which is how plain JavaScript handles errors thrown by callees, fails outright.

import * as React from 'react'
import ReactDOM from 'react-dom'

function ErrorFallback({ error }) {
	return (
		<div role="alert">
			<p>Something went wrong:</p>
			<pre style={{ color: 'red' }}>{error.message}</pre>
		</div>
	)
}

function Greeting({ subject }) {
	return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({ subject }) {
	return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
	try {
		return (
			<div>
				<Greeting />
				<Farewell />
			</div>
		)
	} catch (error) {
		return <ErrorFallback error={error} />
	}
}

ReactDOM.render(<App />, document.getElementById('root'))

React owns the call. Writing in JSX produces an element whose type is the function; it tells React which functions to invoke when App renders, without invoking them. Since the call never happens inside your frame, there is nothing for try/catch to intercept. The imperative nature of try/catch is a second reason to look for a declarative alternative.

Error Boundaries and react-error-boundary

React's Error Boundary feature is that alternative. To qualify as an error boundary, a component must be a class component and implement either getDerivedStateFromError or componentDidCatch. The react-error-boundary package ships that class so you don't have to write it.

import * as React from 'react'
import ReactDOM from 'react-dom'
import { ErrorBoundary } from 'react-error-boundary'

function ErrorFallback({ error }) {
	return (
		<div role="alert">
			<p>Something went wrong:</p>
			<pre style={{ color: 'red' }}>{error.message}</pre>
		</div>
	)
}

function Greeting({ subject }) {
	return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({ subject }) {
	return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
	return (
		<div>
			<ErrorBoundary FallbackComponent={ErrorFallback}>
				<Greeting />
				<Farewell />
			</ErrorBoundary>
		</div>
	)
}

ReactDOM.render(<App />, document.getElementById('root'))

Production output becomes a handled message rather than a blank page.

Scoping and Recovery

An ErrorBoundary behaves much like a try/catch block, minus the imperative plumbing: wrap a large subtree to catch broadly, or narrow it to a single region for finer-grained recovery. react-error-boundary provides the props needed to manage that recovery.

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 Bomb({ username }) {
	if (username === 'bomb') {
		throw new Error('💥 CABOOM 💥')
	}
	return `Hi ${username}`
}

function App() {
	const [username, setUsername] = React.useState('')
	const usernameRef = React.useRef(null)

	return (
		<div>
			<label>
				{`Username (don't type "bomb"): `}
				<input
					placeholder={`type "bomb"`}
					value={username}
					onChange={(e) => setUsername(e.target.value)}
					ref={usernameRef}
				/>
			</label>
			<div>
				<ErrorBoundary
					FallbackComponent={ErrorFallback}
					onReset={() => {
						setUsername('')
						usernameRef.current.focus()
					}}
					resetKeys={[username]}
				>
					<Bomb username={username} />
				</ErrorBoundary>
			</div>
		</div>
	)
}

In the demo, typing bomb swaps the Bomb component for the ErrorFallback. Recovery happens two ways: changing username, which is listed in resetKeys, or clicking "Try again", which calls resetErrorBoundary alongside an onReset that restores state to a value that won't immediately rethrow.

Async and Event-Handler Errors

Error boundaries do not catch errors for:

  • Event handlers
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
  • Server side rendering
  • Errors thrown in the error boundary itself (rather than its children)

The usual workaround is local error state plus a conditional render, which leaves you maintaining two error paths: one for runtime errors, one for failures such as a rejected fetchGreeting. react-error-boundary exposes a hook that removes the split — pass the rejected error to handleError and the library propagates it to the nearest error boundary just like a render error. If a hook already hands you an error value, forwarding the truthy value does the same thing. Either route lets a single boundary handle both synchronous and asynchronous failures.

One Development Caveat

With dev servers that support the overlay — react-scripts, gatsby, codesandbox — you may still see the error overlay even when your Error Boundary handled the error. Production is unaffected.