How do I track errors in a React app, including errors that error boundaries cannot catch?
Error boundaries only catch errors thrown while React renders. Everything else, including event handlers, async code and timers, reaches the browser as a global error or an unhandled promise rejection. Initialise an error-tracking SDK first so it listens for those events, add an error boundary and the React 19 root error hooks, upload source maps, and tag each release.
Why it happens
React's error boundary is a class component that defines getDerivedStateFromError or componentDidCatch. According to the React documentation, componentDidCatch is called when a child component, including a distant one, throws during rendering, and it is meant for logging to an error reporting service. The same documentation lists what boundaries do not catch: event handlers, server-side rendering, errors thrown in the boundary itself, and asynchronous code such as setTimeout or requestAnimationFrame callbacks.
Those uncaught cases are common. A click handler that throws, a fetch that rejects inside a useEffect, a timer callback that reads a missing property: none of them pass through a boundary. They surface to the browser instead, as an error event on window (the basis of window.onerror) or as an unhandledrejection event for a promise nobody caught. If you only wrap your app in a boundary, you will see the render crashes and miss a large share of what users actually hit.
There is a further detail in production. The React documentation says that in development errors caught by componentDidCatch bubble up to window, but in production they do not, so an ancestor handler only receives errors that were not caught by a boundary. That is why you wire up both layers rather than assuming one covers the other. It also notes that component names in the component stack are minified in production and can be decoded with source maps like any JavaScript stack.
How to catch React errors, including the ones boundaries miss
Initialise an SDK in a file that loads first
Sentry's React guide says to create a file such as instrument.js and import it before any other import in your entry point, so the SDK is listening before your app runs (Sentry docs, read 2026-11-13). An SDK that installs global error and unhandled-rejection listeners is what catches the errors boundaries cannot. Read the DSN from build-time configuration, not a hard-coded string.
// instrument.js import * as Sentry from "@sentry/react"; Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, release: import.meta.env.VITE_GIT_SHA, environment: import.meta.env.MODE, }); // main.jsx import "./instrument"; import App from "./App";Wrap the app in an error boundary with a fallback UI
A boundary lets users see something other than a blank page when a render fails, and it reports the error with the component stack. React documents the pattern with getDerivedStateFromError for the fallback and componentDidCatch for reporting. Sentry's React SDK ships an ErrorBoundary component that does both, and Sentry's guide shows it wrapping the app for React 18 and earlier. Place smaller boundaries around risky widgets so one failure does not replace the whole screen.
<Sentry.ErrorBoundary fallback={<p>Something went wrong.</p>}> <App /> </Sentry.ErrorBoundary>On React 19, hook into the root's error callbacks
React 19 lets createRoot and hydrateRoot take onUncaughtError, onCaughtError and onRecoverableError. Sentry's React guide configures all three with Sentry.reactErrorHandler: onUncaughtError for errors no boundary caught, onCaughtError for errors a boundary caught, and onRecoverableError for errors React recovered from by itself (Sentry docs, read 2026-11-13).
const root = createRoot(container, { onUncaughtError: Sentry.reactErrorHandler(), onCaughtError: Sentry.reactErrorHandler(), onRecoverableError: Sentry.reactErrorHandler(), });Cover event handlers and async code explicitly
Because boundaries ignore handlers and async work, rely on the SDK's global listeners for the unexpected ones and use try/catch with a manual capture where you handle failure on purpose, for example around a form submit or a fetch whose error you also show to the user. Always give a promise a catch or use try/catch in an async function, then call the SDK's captureException with the error and any useful tag.
async function onSave() { try { await api.save(form); } catch (err) { Sentry.captureException(err, { tags: { area: "save" } }); setError("Could not save"); } }Record a replay when an error happens
Sentry's React guide enables Session Replay through replayIntegration and samples it with replaysSessionSampleRate and replaysOnErrorSampleRate; its example captures replays for every session that has an error and for a smaller share of all sessions. Replay shows what the user did before the failure, which is often what is missing from a stack trace. Review what the replay records before enabling it on pages that show personal data.
Sentry.init({ integrations: [Sentry.replayIntegration()], replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, });Upload source maps and tag every release
Production stack traces and component stacks are minified until the tracker has the source maps for the exact build that ran. Generate maps in the production build, upload them against a release name, and pass the same release string to init; the full procedure is in the minified stack traces solution linked below. Setting environment separately keeps staging errors out of production views.
| Error source | Caught by an error boundary | What catches it instead |
|---|---|---|
| Throw during render or in a child's lifecycle | Yes | Boundary; on React 19 also the root onCaughtError hook |
| Throw inside an event handler (onClick) | No | try/catch with a manual capture, or the global error listener |
| Rejected promise with no catch | No | The unhandledrejection listener installed by the SDK |
| Error in setTimeout or requestAnimationFrame | No | The global error listener (window.onerror) |
| Error thrown inside the boundary itself | No | A boundary higher in the tree, or the global listener |
| Server-side rendering error | No | Your server-side error tracking |
The boundary column is from the React documentation on error boundaries (react.dev, read 2026-11-13); the fallback column is the approach described in the steps above.
When to use Bugsly
Bugsly accepts events from the standard Sentry browser and React SDKs. Its @bugsly/react package re-exports @sentry/react, so you can keep your code, including ErrorBoundary and reactErrorHandler, and point the DSN at your Bugsly project (https://<public_key>@bugsly.dev/<project_id>). It supports session replay, breadcrumbs, source maps uploaded with sentry-cli, and release tracking, and it groups events into issues and flags regressions.
Once errors arrive, an assistant connected over MCP can read each issue's stack frames and breadcrumbs, and the per-issue AI analysis gives a likely cause as a hypothesis, not a verdict. See the React integration page for setup specifics.
When Bugsly is not the right answer
If you use Next.js, Remix or another server-rendering framework, you need its dedicated SDK because server-side render errors never reach a client error boundary; for Next.js, Bugsly has a separate wrapper. If your data must stay on your own infrastructure, Bugsly is not the right choice: it is hosted and cannot be self-hosted. For React Native, check that the tracker supports your platform before committing.
Ask your AI assistant to do it
With the Bugsly MCP server connected (https://bugsly.dev/api/mcp; setup for ChatGPT, Claude, Cursor and Gemini CLI is on /mcp), paste this. The assistant reads first and reports back; you decide what happens next.
Assign to Bugsly: use list_projects to find my React project, then list_issues for it and pick the 3 most recent unresolved issues. For each, call get_issue_events and tell me whether the error came from a render crash, an event handler, or an unhandled promise rejection, based on the stack and breadcrumbs, and whether the top frame shows my original source file. Do not resolve or ignore anything; show me what you found so I can approve the next step.
Frequently asked questions
Why does my error boundary not catch an error in an onClick handler?
React error boundaries only catch errors thrown during rendering and lifecycle. Errors in event handlers do not pass through them, so use try/catch with a manual capture or rely on a global error listener from your SDK.
Do unhandled promise rejections reach error boundaries?
No. Asynchronous code is not caught by a boundary. A rejection with no catch surfaces as an unhandledrejection event on window, which an error-tracking SDK listens for.
Do I need both an error boundary and global handlers?
Yes. The boundary gives users a fallback screen and a component stack for render errors; the global listeners catch what the boundary structurally cannot.
Is session replay safe to enable?
It records page content, so review masking before turning it on, especially on pages with forms or personal data. Sampling only on error limits how much is recorded.
Sources
- React docs: Component (error boundaries) (read 2026-11-13)
- Sentry docs: React (read 2026-11-13)
Related
- Bugsly React integration
- Best React error tracking tools
- Readable stack traces with source maps
- Keep sensitive data out of error reports
- All problems and solutions
Sentry is a trademark of its owner; Bugsly is not affiliated with or endorsed by Sentry. “Sentry-compatible” only means Bugsly accepts events sent by Sentry's open SDKs.