Direct answer
A React error boundary catches rendering errors thrown by any component in its subtree during render, in lifecycle methods, and in constructors, logs the error with enough context to diagnose it, and shows a fallback UI instead of leaving the user with a blank screen or React's own default error overlay; it does not catch errors in event handlers, asynchronous code, or errors thrown in the boundary component itself.
Structured elaboration
What it catches. Errors thrown during the render phase of any component below the boundary in the tree, including errors in lifecycle methods (componentDidMount, etc.) and in constructors. This is React's mechanism for preventing one broken component from crashing the entire application.
What it does NOT catch, and why that matters. Event handlers (a click handler that throws is a normal JavaScript exception, not a React rendering error, and needs its own try/catch); asynchronous code (a .then() callback or an async function's rejection happens outside React's render cycle entirely); server-side rendering errors; and errors thrown by the error boundary component itself (a boundary cannot catch its own failures, which is why the boundary component should be kept as simple as possible, with minimal logic that could itself throw).
Logging to an error-tracking service. componentDidCatch(error, info) receives both the error object and a componentStack describing which component tree led to the failure; sending both to a service like Sentry, tagged with any available user or session context, turns "a customer reported a blank page" into "we can see exactly which component threw, with what stack, for which user" without waiting for the customer to describe what they were doing.
Retry UI versus surfacing the raw error. A "try again" button that resets the boundary's state and re-attempts rendering the subtree is appropriate when the failure might be transient (a component that failed because of a momentary bad prop from a slow API response); it is misleading for a deterministic bug that will fail identically on every retry, where a generic "something went wrong, we've been notified" message (with no false promise that retrying will help) is more honest to the user, even though it is less satisfying than a button that appears to offer control.
Worked example
jsx
class ErrorBoundary extends React.Component {
constructor(props) { super(props); this.state = { hasError: false }; }
static getDerivedStateFromError(error) { return { hasError: true }; }
componentDidCatch(error, info) {
if (this.props.logger) this.props.logger.logError(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return <div role="alert">{this.props.fallbackText || 'Something went wrong.'}</div>;
}
return this.props.children;
}
}
Executed (React Testing Library, verified): rendering <ErrorBoundary logger={logger} fallbackText="We hit a snag. Please retry."><Boom /></ErrorBoundary>, where Boom throws during render, confirms screen.getByRole('alert') shows the fallback text and logger.logError was called exactly once with the thrown Error object. A second test confirms that when no child throws, the boundary renders its children normally and logger.logError is never called, so the boundary is confirmed to be transparent in the non-error case, not just functional in the error case.
Trade-offs and pitfalls
A single application-wide error boundary at the root catches everything but takes down the ENTIRE page for a failure in one small, non-critical widget; placing boundaries around individual independent sections (a sidebar widget, a comments section) means one broken component degrades gracefully to just that section showing a fallback, while the rest of the page keeps working, which is almost always the better default for anything with multiple independent sections. The most common mistake is assuming an error boundary catches an async data-fetching failure inside a useEffect: it does not, since that error occurs outside the render phase entirely, and needs its own explicit error state managed by the component, separate from the boundary mechanism.