Overview

An error boundary is the React equivalent of try/catch for rendering: it catches errors thrown while rendering its descendants and shows a fallback instead of the broken subtree. React 19 still has no function-component form; a boundary needs getDerivedStateFromError and componentDidCatch on a class. Boundaries cover one surface of the error story; data libraries, route error files, and explicit try/catch cover the rest.

Expect boundaries to catch render errors, with one async exception

A boundary does not catch errors in event handlers, in async callbacks such as setTimeout or fetch().then, in server-side rendering (the framework handles those), or in the boundary itself. Errors thrown inside startTransition do reach the nearest boundary. For event handlers, catch the error and either store it in state or hand it to the boundary with useErrorBoundary().showBoundary(error).

Use react-error-boundary instead of writing the class

react-error-boundary (v6 requires ES module support; use v5 otherwise) ships the class plus the reset and hook APIs. Pass one of fallback (static element), fallbackRender, or FallbackComponent.

import { ErrorBoundary } from "react-error-boundary";
 
<ErrorBoundary
  resetKeys={[postId]}
  onError={(error, info) => report(error, info.componentStack)}
  fallbackRender={({ resetErrorBoundary }) => (
    <div role="alert">
      <p>Could not load comments.</p>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )}
>
  <Comments postId={postId} />
</ErrorBoundary>

Without the dependency, write one reusable class once with static getDerivedStateFromError(error) returning { error } and a componentDidCatch(error, info) that calls your reporter. Do not hand-roll a new boundary per route.

Choose one data-error strategy per surface

With TanStack Query v5, useSuspenseQuery throws to the nearest boundary, and useQuery does so only with throwOnError: true. Use QueryErrorResetBoundary so a retry refetches instead of re-reading the cached failure. Use inline error state when the component can recover locally (a retry button, cached fallback), and throw to a boundary when the whole region fails. Mixing both on the same data is confusing.

Design fallbacks that recover

  • Name the failed region (“Could not load comments”), not a generic “Something went wrong.”
  • Offer an action the user can take: retry, a link to the rest of the page, or a way out of the flow.
  • Never render the raw error message in production; it leaks internals.
  • Match the missing region’s size so the page does not jump; see core-web-vitals.

Log from the root hooks and the boundary

React 19 createRoot and hydrateRoot accept onCaughtError (caught by a boundary), onUncaughtError, and onRecoverableError. Report from these for app-wide coverage, and from a boundary’s onError for region context. Sentry provides Sentry.reactErrorHandler() for these hooks.

createRoot(el, {
  onUncaughtError: Sentry.reactErrorHandler(),
  onCaughtError: Sentry.reactErrorHandler(),
  onRecoverableError: Sentry.reactErrorHandler(),
});

Scrub PII with beforeSend: form values can land in breadcrumbs, so also follow the secret-handling rule in react-forms. See error-tracking for reporter setup.

Reset with resetKeys, and place boundaries at Suspense seams

A boundary that retries the same broken input throws again. resetKeys={[userId]} clears the error when the input changes, and resetErrorBoundary clears it on demand. Put one boundary at each independent region, outside the matching <Suspense>, so loading and failure are handled at the same seam.

<ErrorBoundary fallback={<MainError />}>
  <Suspense fallback={<MainSkeleton />}><Main /></Suspense>
</ErrorBoundary>

Use the Next.js file conventions in the App Router

error.tsx is a route-segment boundary and must be a Client Component. It receives error and retry; retry() (stable in Next.js 16.3) refetches and re-renders the segment’s children, while reset() clears error state without refetching. global-error.tsx replaces the root layout and must render its own <html> and <body>. For a component-level boundary that does not interfere with notFound() or redirect(), use catchError from next/error (Next.js 16.3). See nextjs and react-suspense.