Command Palette

Search for a command to run...

Hectal
PHASE 23Advanced ~27 min· topic 5 of 6

Topic 23.5

Monitoring and Error Reporting

In one line

In production you can't see your users' consoles, so errors must report themselves: React 19's root callbacks (onCaughtError, onUncaughtError, onRecoverableError) plus window error and unhandledrejection listeners send every crash to an error tracker, source maps turn its minified stack traces back into your code, and analytics and logs are collected with consent and without personal data.

Think of it like this

A smoke alarm in every room. You can't stand in every room watching for fire, so each alarm reports by itself the moment something happens, and tells you which room it is. Without alarms, you find out about a fire when a neighbour phones. Without error reporting, you find out about a crash when an angry customer writes in, days later, with no details.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Monitoring
Collecting signals from production (errors, performance, logs, usage) to know what's really happening.
Error tracker
A service that collects error reports, groups them, counts affected users and alerts you.
onCaughtError / onUncaughtError
React 19 root options called when a render error was caught by a boundary, or not caught at all.
onRecoverableError
A root option called when React recovered from an error by itself, such as a hydration mismatch.
Component stack
The list of components from the one that threw up to the root, in errorInfo.componentStack.
unhandledrejection
A window event fired when a promise is rejected and nothing handles it.
Release
A name for one build (a version or commit) attached to every report and to its source maps.
Breadcrumbs
The last few things that happened before an error: clicks, navigations, requests.
PII
Personally identifiable information: names, emails, phone numbers, addresses and anything that identifies a person.
Consent
The user's permission to collect non-essential data such as analytics.

Step by step

01Where each kind of error goes

Render errors go through React (and boundaries, if you have them). Everything else goes to the window. A complete setup listens in all four places.

Where each kind of error goesdiagram
Rendering diagram…

02Wire the root callbacks to a tracker

Initialise the tracker SDK first, then pass React's callbacks. Sentry, for example, provides Sentry.reactErrorHandler() for these options; other trackers have their own helper, or you call their captureException yourself. Check your tracker's React 19 guide for its current API.

src/main.jsx (Sentry example)whole filejsx
import * as Sentry from "@sentry/react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN, // public by design
  release: import.meta.env.VITE_RELEASE, // e.g. the git commit, set in CI
  environment: import.meta.env.MODE,
  sendDefaultPii: false,
});

createRoot(document.getElementById("root"), {
  onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
    console.warn("Uncaught error", error, errorInfo.componentStack);
  }),
  onCaughtError: Sentry.reactErrorHandler(),
  onRecoverableError: Sentry.reactErrorHandler(),
}).render(<App />);

03Upload hidden source maps in CI

Build with hidden maps, upload them with the same release name, then delete them from dist/ so they're never deployed. Most trackers offer a Vite plugin or a CLI for this; the plugin below is Sentry's (option names change between versions, so check its docs).

vite.config.js (example)whole filejsx
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";

export default defineConfig({
  build: { sourcemap: "hidden" },
  plugins: [
    react(),
    sentryVitePlugin({
      org: "my-org",
      project: "shop-web",
      authToken: process.env.SENTRY_AUTH_TOKEN, // CI secret: Node only, never VITE_
      sourcemaps: { filesToDeleteAfterUpload: ["./dist/**/*.map"] },
    }),
  ],
});

04Read a report

With maps uploaded, the tracker turns minified frames into source frames. Without them you get the first line only. The report below is illustrative.

Error tracker (illustrative)whole filebash
TypeError: Cannot read properties of undefined (reading 'price')
  release: shop@1.4.2   environment: production   users affected: 37
  route: /cart          browser: Chrome 141 / Android

  without source maps:  at d (assets/index-D9PM2J-I.js:1:48213)
  with source maps:     at CartTotal (src/cart/CartTotal.jsx:14:22)
                          const total = items.reduce((sum, item) => sum + item.product.price, 0);

  breadcrumbs: navigation /products -> /cart, fetch GET /api/cart 200, click "Apply coupon"

05Collect analytics with consent

Load analytics only after consent, scrub personal data before anything leaves the browser, and batch events. The live example below shows each rule.

Try it yourself

  1. 1

    Remove the boundary

    In the onCaughtError example, render <Buggy crash={crash} /> without <Boundary>. Predict which callback runs now and what the page shows.

  2. 2

    Handle the rejection

    In "Errors React never sees", change the second handler to Promise.reject(new Error("Network failed")).catch(() => console.log("handled")). Which lines disappear from the console, and why?

  3. 3

    Withdraw consent

    In the analytics example, after the third click, click consent again to switch it off, then click Search. What happens to the event in the queue? Is that what a user who withdrew consent would expect?

Code & diagrams

onCaughtError: a boundary caught it, report it anyway React 19+ New tab

The user saw the boundary's fallback, and the reporter still heard about it. Because `onCaughtError` is set, React no longer prints its own "The above error occurred..." message (compare the last example in Topic 23.3). The component stack names `Buggy`, which tells you where to look.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[reporter] caught Price is undefined | stack has Buggy: true
page: Break widgetWidget failed.
onUncaughtError: no boundary, the whole tree is gone React 19+ New tab

An uncaught render error unmounts everything inside the root: a blank page. Report it with the release and route, and show a message outside React (or, better, add error boundaries so it never gets this far). Without the callback, React would print `Uncaught Error: Cart total is NaN` plus advice to the console, where no one in production would see it.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[tracker] {"level":"fatal","message":"Cart total is NaN","component":"Cart","release":"shop@1.4.2","path":"/cart"}
root is empty: true
fallback: Something went wrong. Please reload.
Errors React never sees: handlers and promises New tab

The `Uncaught` lines are what the browser prints by itself. Neither error unmounted the app, and no error boundary or root callback saw them: only the window listeners did. Error tracker SDKs register these two listeners for you.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Uncaught Error: Save button exploded
[reporter] window error: Save button exploded
Uncaught (in promise) Error: Network failed
[reporter] unhandled rejection: Network failed
app still on screen: true
Analytics with consent, scrubbing and batching New tab

The first click happens before consent, so nothing is recorded. After consent, the `email` field is dropped and an email typed into a search box is masked before it leaves the browser. Events go out three at a time, and the rest on leaving. Real scrubbing needs more patterns (phone numbers, tokens in URLs); do it on the server as well.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

dropped (no consent): add_to_cart
sent batch: [{"event":"add_to_cart","productId":"mango-1kg"},{"event":"search","query":"mail me at [email]"},{"event":"add_to_cart","productId":"mango-1kg"}]
sent batch: [{"event":"search","query":"mail me at [email]"}]

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Expect a boundary to catch a click error

Wrap a form in an error boundary and throw inside its onSubmit handler, expecting the boundary's fallback to show.

terminal
$ Console
── what you'll see ──
Uncaught Error: Save button exploded
(the form stays on screen, the boundary's fallback never appears, onCaughtError is never called)

Break #2

Upload no source maps

Deploy with an error tracker but without uploading source maps for the release.

terminal
$ Error tracker
── what you'll see ──
TypeError: Cannot read properties of undefined (reading 'price')
at d (assets/index-D9PM2J-I.js:1:48213)
at Rl (assets/index-D9PM2J-I.js:1:91877)

Myth vs fact

Myth

Error boundaries catch all errors in my components.

Fact

They catch render, lifecycle and effect errors. Event handlers, timers and unhandled promises need window listeners or explicit try/catch.

Myth

Uploading source maps to a tracker makes my code public.

Fact

Hidden maps uploaded to a tracker are private to your account. Public sourceMappingURL comments are what expose source; deleting maps from dist/ avoids that.

Myth

Anonymous analytics can't contain personal data.

Fact

URLs, search terms, error messages and free-text fields often contain emails, names or IDs. Scrub them deliberately, both in the browser and on the server.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Passing onUncaughtError/onCaughtError replaces React's default console reporting. Keep a console.error in development (or call reportError(error), which also fires the window error event) so you don't lose local visibility.

  • ▸

    onRecoverableError is where hydration mismatches show up in production (Topic 20.6). They don't crash the page but cost performance, because React throws away the server HTML for that part and renders it on the client. Track their rate after each release.

  • ▸

    Group and alert on new issues per release and on regressions (an issue that was resolved and returns), not on raw counts. Set sample rates for performance traces and session replays, which are far larger than error events, and mask all inputs in replays by default.

  • ▸

    Chunk load errors after a deploy (Topic 23.3) and errors from browser extensions are the classic noise in front-end trackers. Filter extension frames (chrome-extension://), and handle chunk errors with a reload instead of reporting each one as a bug.

Remember this

  1. 1

    Why monitoring. Your app runs on thousands of devices you'll never see, with browsers, extensions, networks and data you never tested. Monitoring means collecting signals from production so you know what's happening: errors (what broke, where, for how many users), performance (Topic 23.4), logs (a record of notable events) and product analytics (what people use). An error tracker (Sentry, Bugsnag, Rollbar, Datadog RUM, or your own endpoint) groups similar errors, counts affected users, and alerts you when something new or frequent appears.

  2. 2

    React's root error callbacks (React 19). createRoot(container, options) and hydrateRoot accept three callbacks. **onCaughtError(error, errorInfo) runs when an error boundary caught a render error (the user saw your fallback UI). onUncaughtError(error, errorInfo) runs when no boundary caught it, so React unmounted the whole tree: the screen is now empty. onRecoverableError(error, errorInfo)** runs when React recovered by itself, typically a hydration mismatch it fixed by re-rendering on the client (Topic 20.6). errorInfo.componentStack lists the components from the one that threw up to the root, which is often more useful than the JavaScript stack. When you pass a callback, it replaces React's default console logging for that kind of error.

  3. 3

    What error boundaries and root callbacks don't see. Boundaries (Topic 14.5) only catch errors thrown while rendering, in lifecycle methods and effects. Errors in event handlers, setTimeout callbacks and rejected promises that nothing awaits never pass through React. Catch those with window.addEventListener("error", ...) and window.addEventListener("unhandledrejection", ...). Error tracker SDKs install both listeners for you, and you add the React callbacks on top.

  4. 4

    Source maps make stack traces readable. A production error looks like at d (index-D9PM2J-I.js:1:48213). Build with build.sourcemap: "hidden" (Topic 23.1), upload the .map files to the tracker during CI, tagged with a release name (a version or git commit), and don't publish them on your site. The tracker then shows at formatPrice (src/money.js:2:30) with the source line. The release must match exactly: the tracker uses it to choose the right maps, and to tell you in which deploy an error first appeared, so you can roll back (Topic 23.3).

  5. 5

    Logs and context. A useful error report has context: the release, the route, the browser, the last few user actions (breadcrumbs: "clicked Pay", "GET /api/cart 500"), and a user or session ID. Add context, but never passwords, tokens, card numbers or full form contents. Keep logs structured ({ level, message, ...fields }), sample noisy events, and filter errors you can't act on (browser extensions, ResizeObserver loop noise, bots) so real problems stand out.

  6. 6

    Analytics and privacy. Product analytics answers "what do people use?". Privacy law (GDPR in Europe, India's DPDP Act and others) and basic respect require care: ask for consent before non-essential tracking, collect the minimum (events and anonymous IDs, not names and emails), scrub personal data that slips into URLs, search queries and error messages, set retention limits, and respect opt-outs. Prefer first-party, privacy-focused tools when you can, and remember that error reports are data about people too. Send events in batches and flush on visibilitychange (when the tab is hidden) with navigator.sendBeacon, which is reliable when the page closes.

Explain it without notes

01

What are onCaughtError, onUncaughtError and onRecoverableError, and when does each run?

02

Which errors do error boundaries miss, and how do you report them?

03

How do source maps make production error reports useful, and how should you deploy them?

04

How would you add analytics to a React app while respecting user privacy?

Practice

01

Write a main.jsx that reports all three kinds of React root errors and both window error events to a function report(kind, error, extra).

02

Write a scrubUrl(url) function that removes the email and token query parameters before a page view is sent to analytics, and test it with /reset?email=asha@example.com&token=abc&step=2.

Trade-offs

  • ↔

    A hosted error tracker gives grouping, alerts and source map handling in minutes but sends data to a third party; a self-hosted or home-made endpoint keeps data in-house but you build the grouping and alerting.

  • ↔

    More context (breadcrumbs, replays, user IDs) makes bugs faster to fix and raises privacy risk and cost; collect the least that answers your questions, and mask by default.

Done when you can

  • I can report render errors with onCaughtError, onUncaughtError and onRecoverableError.

  • I report event handler and promise errors with window listeners.

  • I can build hidden source maps, upload them with a release name and keep them off the site.

  • I add useful context to reports without personal data.

  • I collect analytics only with consent, scrubbed and batched.