React error monitoring only works if you can see the errors your error boundaries hide. Most production failures in a React app never reach a dashboard. They happen inside event handlers, async code, and promise rejections that boundaries were never built to catch. Middleware RUM captures those errors as they happen, unminifies the stack, and ties each one to the backend request that caused it.

See every error in one timeline

Start a 14-day free trial with unlimited ingestion and see every runtime error, console error, and failed network call from your app in one session.

Key takeaways

  • React error monitoring captures, groups, and diagnoses errors in a live React app, then connects each one to the code and backend call that produced it.
  • Error boundaries only catch errors thrown during rendering, lifecycle methods, and constructors, so event handlers, async code, and promise rejections slip through untracked.
  • Minified production bundles turn stack traces into noise; source maps are what make an alert actionable instead of a guess.
  • Middleware RUM captures runtime errors, console errors, and failed network calls, groups them by signature, and ties each one to the session that produced it.
  • Trace propagation links a browser error to the exact backend span behind it, replacing “cannot reproduce” tickets with a direct path to the failing API.
  • The same session telemetry that surfaces errors also drives Core Web Vitals and long-task data for real user performance optimization.

What is React error monitoring?

React error monitoring is the process of capturing, grouping, and diagnosing errors in a live React app, then linking each one to its source code, session, route, release, and browser.

This is different from local debugging in three ways. The errors come from real devices and real networks, not your laptop. The code is minified, so raw stack traces are unreadable without source maps. And the error is usually a symptom, not the root cause. The real question is what changed upstream.

A full setup for frontend error monitoring answers five questions for every incident:

  • What broke? The error message, type, and unminified stack trace.
  • Who is affected? Distinct users, not raw instance counts.
  • When did it start? First occurrence, mapped against your deploy timeline.
  • What was the user doing? The route, the clicks, the requests in flight.
  • What caused it? The backend span, the failed chunk, or the release behind it.

Tools that answer only the first two questions are error trackers. Tools that answer all five are what teams need when a release breaks at 2am.

Why React errors are hard to see in production

React errors stay hidden in production for three main reasons: error boundaries cover only part of the code, failures fail silently, and stack traces arrive minified. A React app that runs fine on your laptop can quietly fail for a third of your users. The reasons are structural, not accidental.

Error boundaries have a narrow blast radius. They only catch errors in rendering, lifecycle methods, and constructors below them. Event handlers, setTimeout, async functions, and server-side rendering are not covered. That’s most of the code your users actually touch.

Failures are silent by design. A caught error swaps in fallback UI. The user sees a blank card. Your team sees nothing, because nothing was ever sent anywhere.

Stack traces arrive minified. A production error looks like t is not a function at a.min.js:1:48219. Without source maps, you can’t map that back to a component.

Routing hides the context. A single-page app changes its URL without a page load. If your monitoring only tracks the first load, every route after that is invisible.

Async and network failures never throw. A rejected fetch, a 500 from an API, a dropped mobile request: none of these throw a React error. They just produce an empty list and a confused user.

The result is familiar. Your APM dashboard looks healthy, support tickets pile up, and nobody can connect the two.

The error classes worth tracking in a React app

The main React error classes are render errors, event handler errors, unhandled promise rejections, ChunkLoadError, network and XHR errors, hydration mismatches, and console errors. Before you instrument anything, know what you’re chasing. This is where React error tracking starts: classify errors by type instead of reading every stack by hand. Middleware RUM automatically sorts browser errors into these classes.

Error classWhat it looks likeWhere it comes from
Render errorsCannot read properties of undefinedA component receiving a shape it didn’t expect
Event handler errorsClick does nothing, no fallback UIErrors thrown inside onClick, onSubmit, and similar handlers
Unhandled promise rejectionsUncaught (in promise) TypeErrorMissing catch on async work
ChunkLoadErrorLoading chunk 3527 failedCode splitting plus stale CDN caches after a deploy
Network and XHR errorsStatus 0, 4xx, 5xxCORS, auth expiry, backend faults, ad blockers
Hydration mismatchesText content did not matchServer rendered markup diverging from client render
Console errorsconsoleError entriesThird party scripts, deprecation warnings, swallowed failures

Middleware records error.name, error.message, error.stack, error.type, browser and OS details, session.id, and page.href for every error. Each of these becomes a filter, not a search. See what real user monitoring is and how it works for the full picture.

Classify errors automatically

Add Middleware RUM to your React app and stop parsing raw stack traces by hand.

Instrumenting a React app with Middleware RUM

You control the base HTML in a React project: public/index.html in Create React App, or index.html in Vite. Add the SDK there so it loads before your bundle runs.

<script
  src="https://cdnjs.middleware.io/browser/libs/0.0.2/middleware-rum.min.js"
  type="text/javascript"
  crossorigin="anonymous"
></script>

Keep this tag ahead of your app bundle and any analytics scripts. Otherwise, early errors happen before anything is listening.

Then initialize the SDK:

<script>
  if (window.Middleware) {
    Middleware.track({
      projectName: "my-application",
      serviceName: "mw-application",
      accountKey: "your-account-token",
      target: "https://<UID>.middleware.io",
      env: "production",
      defaultAttributes: {
        "app.version": "1.4.2"
      }
    });
  }
</script>

Two fields matter most. env separates staging noise from production signal. app.version matches an error to a release and to the right source map, so wire it to your build pipeline instead of hardcoding it.

Route changes need no extra code. The SDK treats History API updates as new views, so soft navigations are measured like page loads. The one exception is a memory-only router, which never updates the URL.

Full setup reference: Browser RUM for ReactJS. Next.js teams should use the NextJS guide instead.

Tracking errors in React: error boundaries and beyond

An error boundary is a React component that catches JavaScript errors in the child components below it during rendering, lifecycle methods, and constructors, then shows fallback UI instead of crashing the app. The SDK captures uncaught runtime errors, console errors, and resource failures automatically. The gap is everything React catches on your behalf, then hides.

Report from your error boundary

import { Component } from "react";

class ErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    if (window.Middleware) {
      window.Middleware.error(error);
      window.Middleware.info(
        `componentStack: ${errorInfo.componentStack}`
      );
    }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? <p>Something went wrong.</p>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

The component stack is what’s worth keeping. A minified stack trace tells you which function failed. The component stack tells you which part of the UI the user was looking at.

Place boundaries at meaningful seams, around each route, around each independently loaded widget. One failing dashboard card should degrade that card, not blank the whole page.

Use the React 19 root handlers

On React 19, createRoot accepts error callbacks for the whole tree, including errors a boundary already handled.

import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"), {
  onUncaughtError: (error) => window.Middleware?.error(error),
  onCaughtError: (error) => window.Middleware?.error(error),
  onRecoverableError: (error) => window.Middleware?.warn(String(error))
});

This is the cleanest way to stop losing handled errors. They’re the ones that quietly break a feature for weeks.

Catch async failures explicitly

async function loadInvoices(customerId) {
  try {
    const res = await fetch(`/api/customers/${customerId}/invoices`);
    if (!res.ok) {
      throw new Error(`Invoice fetch failed with status ${res.status}`);
    }
    return await res.json();
  } catch (error) {
    window.Middleware?.error(error);
    throw error;
  }
}

Middleware.error() accepts a string or an Error object. Pass the Error when you have one, so the stack survives. info, warn, and debug cover the surrounding context.

Unminifying React errors with source maps

A source map is a file that maps minified production code back to your original source, so a stack trace shows real file names and line numbers instead of a jumble of single letters. Uploading them is the step teams skip, and the step that decides whether error monitoring is useful at all.

Enable source maps in your build:

// webpack.config.js
module.exports = {
  devtool: "source-map"
};

// next.config.js
module.exports = {
  productionBrowserSourceMaps: true
};

Install the uploader once:

npm install -g @middleware.io/sourcemap-uploader

Upload after every build, using the same version string you set in app.version:

sourcemap-uploader upload 
  --apiKey=<rum_account_key> 
  --path="./build/static/js" 
  --appVersion="1.4.2"

Prefer a plugin? Webpack and Next.js can upload automatically at the end of the build:

const MiddlewareWebpackPlugin =
  require("@middleware.io/sourcemap-uploader/dist/webpack-plugin").default;

const nextConfig = {
  productionBrowserSourceMaps: true,
  webpack: (config) => {
    config.plugins.push(new MiddlewareWebpackPlugin("<ACCOUNT_KEY>"));
    return config;
  }
};

Two things break source maps in practice. First, –apiKey is your RUM account key, not your general API key. Second, –appVersion at upload must equal defaultAttributes[“app.version”] at init, or the ORIGINAL view keeps showing minified frames. Full options and troubleshooting live in Error Debugging.

Once maps are uploaded, the Crash Trace view offers an ORIGINAL and MINIFIED toggle. Original frames show real file names, line numbers, and column numbers. For code-split apps, skip the runtime and chunk loader frames, and start at the first frame that belongs to your code.

Security note: upload the maps, then strip them from what you serve. You get readable stacks internally, without publishing your source to anyone who opens devtools.

Enriching errors with user, session, and application context

Errors without context generate meetings. Errors with context generate fixes.

An error group alone tells you a component threw. The same group filtered by release, plan tier, browser, and route usually tells you why, in one pass. Middleware collects device, browser, OS, geography, route, and session automatically. What you add is the context only your app knows.

import { useEffect } from "react";

export default function App({ session }) {
  useEffect(() => {
    if (window.Middleware && session) {
      window.Middleware.setAttributes({
        user_type: session.role,
        plan: session.plan,
        workspace_id: session.workspaceId,
        feature_flags: session.enabledFlags.join(",")
      });
    }
  }, [session]);

  return /* your app */;
}

Call setAttributes after track, and again whenever identity changes, like a workspace switch. Three attributes earn their place in almost every app:

  • Release version. Set through app.version at init. Without it, you can’t tell a regression from a long-standing bug, and source maps won’t resolve.
  • User or account ID. Unlocks the Affected Users metric, the number that tells you whether 40,000 instances is a real outage or one retrying tab.
  • Feature flag state. When an error only shows up in a rollout cohort, this turns a two-day investigation into a filter.

Keep the payload small and stable. Use attributes to slice error groups, not to store app state. The sanitizing rules below apply to everything you add here.

Sanitizing error messages to prevent data leaks

Error monitoring quietly moves data out of the browser. An error message is a string your code composes. It carries whatever you put into it, and it gets stored, searched, and shared through deep links.

Three habits prevent almost every leak.

Don’t interpolate user data into error messages. Reference identifiers, not values.

// Leaks the value into the error group name and search index
throw new Error(`Invalid email: ${form.email}`);

// Safe: identifies the failure without carrying the data
throw new Error("Invalid email format on signup form");

Scrub before you report. When you must attach detail, run it through a redactor first.

const SENSITIVE = ["token", "password", "email", "authorization", "card"];

function redact(payload) {
  return Object.fromEntries(
    Object.entries(payload).map(([key, value]) => [
      key,
      SENSITIVE.some((s) => key.toLowerCase().includes(s))
        ? "[redacted]"
        : value
    ])
  );
}

window.Middleware?.error(error);
window.Middleware?.info(`context: ${JSON.stringify(redact(context))}`);

Keep secrets out of the collected surface. Use ignoreUrls for endpoints whose query strings carry tokens, and ignoreHeaders for headers you don’t want captured. Both are set at init.

Session replay is the other half of this. Middleware masks sensitive inputs by default. You can harden it further at init, so masking happens client-side before anything is sent:

Middleware.track({
  /* existing config */
  recording: "1",
  recordingOptions: {
    maskAllInputs: true,
    maskTextSelector: ".pii, [data-pii]",
    blockSelector: ".auth-widget, .payment-form"
  }
});

Gate initialization on your consent signal. Set recording: “0” when a user declines replay but allows analytics. You still get browser traces and errors, just no video. Details in Session Recording Privacy and Data Security.

One governance point that’s easy to miss: original stack traces expose internal file paths and module names. Share error links with teammates only, never in public channels or vendor tickets.

Grouping, investigating, and resolving React errors with Middleware Error Tracking

Open Real User Monitoring, then Applications, then Error Tracking. This is the core of react error tracking day to day: the Errors list groups instances by message and stack signature, so one regression is one row, not forty thousand.

Each row shows the error name, type, instance count, first and last occurrence, and affected users. The trend chart tells you when the spike started, which usually tells you which deploy caused it.

A working triage loop:

  1. Set the time range around the spike and click the peak on the trend chart.
  2. Filter by App Version and env to isolate the release.
  3. Sort by Affected Users, not Instances. User impact drives priority; instance count mostly measures retry loops.
  4. Open the group and switch Crash Trace to ORIGINAL. Start at the first frame in your own code.
  5. Open Relevant Sessions and launch the session player at the error timestamp. Watch the clicks before it, and check the Network tab for the failed request.
  6. Share the deep link, so the owning engineer sees the same view instead of a screenshot.

Steps four and five turn a browser error from a string into something you can reproduce. Use the Properties panel to check details like browser, device, or user cohort. See Error Tracking for the full attribute reference.

“We had an issue where an order’s cart was associated with the wrong event, creating a conflict in our database. Using Middleware’s session replay, we quickly identified that an unchecked front-end component allowed users to create garbage data. This allowed us to resolve the issue quickly.”

Rangaraj Tirumala, Founding Engineer at Hotplate

Connecting frontend errors to backend root cause

Trace propagation passes a shared trace ID from a browser request to the backend service that handles it, so the frontend error and the backend span that caused it appear on the same timeline. Most React errors are symptoms. The cause is a service that timed out, a schema that changed, or a query that got slow after a migration. Connecting the two is what standalone error trackers can’t do.

Add trace propagation to your RUM init:

<script>
  if (window.Middleware) {
    Middleware.track({
      projectName: "mw-application",
      serviceName: "my-application",
      accountKey: "<account-key>",
      target: "https://<UID>.middleware.io",
      env: "production",
      tracePropagationTargets: [/localhost:3000/i, /api.domain.com/i],
      tracePropagationFormat: "b3"
    });
  }
</script>

You need a Middleware APM agent running in the backend to complete the circuit. Some language SDKs also need OTEL_PROPAGATORS=b3 to accept the propagated context. Once it’s wired, a failed fetch in the browser and the span that served it sit on the same timeline. You can open Session Replay directly from the trace list.

This is the practical argument for keeping frontend and backend telemetry in one platform. The alternative is a browser error in one tool, a trace in another, and an engineer matching timestamps by hand during an incident.

Worked example: ChunkLoadError after a deploy

A ChunkLoadError happens when the browser asks for a JavaScript chunk that no longer exists on the server, usually because a new deploy renamed the file while a user’s cached page still points to the old one. Here’s what that looks like: you ship a release, and ten minutes later, Loading chunk 3527 failed climbs the Errors list.

What’s happening: users on the old build request a lazy-loaded chunk that no longer exists at that hash. Their HTML shell is cached, and the new build renamed the file.

How to confirm it:

  1. Filter Error Type to error and search for “Loading chunk.”
  2. Check first occurrence against your deploy time. They’ll match.
  3. Open Relevant Sessions and check Network. The chunk request shows a 404, or status 0 if it was blocked.
  4. Confirm affected users cluster on the previous App Version.

Fixes worth considering: keep old chunks on the CDN for a grace period, set cache headers so the HTML shell revalidates while hashed assets stay immutable, wrap import() in a retry that reloads the page once, and generate a build ID from your Git hash.

Then verify. Filter by the new App Version and watch instances fall to zero. If they don’t, the fix didn’t ship.

Worked example: an XHR error that looks like a frontend bug

Support reports that saving a profile fails “sometimes.”

  1. Filter Error Type to xhr in Error Tracking.
  2. Open the group and read the request URL and status in Properties.
  3. Jump to a Relevant Session and open the Network tab around the timestamp.
  4. If tracing is enabled, follow the request into APM.

A status of 0 usually means the request never left the browser: a CSP block, a corporate proxy, an ad blocker, or a failed CORS preflight. A cluster of 401s usually means token expiry with no refresh path. A 500 means it was never a frontend bug at all, and the trace will name the service.

From error monitoring to real user performance optimization

Errors are the loudest failures, not the most common ones. The same session stream that surfaces a TypeError also shows the slow route that made users abandon checkout, without a single error being thrown.

Middleware RUM collects both from one SDK. That makes real user performance optimization a filtering exercise, not a second integration:

  • Core Web Vitals per route. Filter by view.url to find which templates miss Google’s thresholds: LCP at or under 2.5 seconds, CLS at or under 0.1, INP at or under 200 milliseconds. An app average hides the one route that’s actually failing.
  • Long tasks. Main-thread blocks over 50 milliseconds are recorded automatically. Clusters usually point at an expensive render, an unmemoized list, or a heavy third-party script.
  • Resource timings. Every XHR, fetch, image, and script arrives with detailed timing phases, which separates a slow API from a slow bundle.
  • Frustration signals. frustration_count on a view is the practical bridge between errors and performance. Rage clicks with no error usually mean a handler that fails silently, exactly the bug an error boundary would never catch.

The workflow is the same one you use for errors: find the worst route, open a session replay, watch what the user experienced, then follow the slow request into the backend trace. See Page Performance for the metric definitions.

Keeping the signal clean

Error monitoring fails in two directions. Too little data, and you miss regressions. Too much, and nobody reads the list.

Sample sessions, not events. Use SessionBasedSampler so a sampled journey stays whole. Sampling individual events leaves you with orphaned errors and no path to reproduction.

Middleware.track({
  serviceName: "service_name",
  accountKey: "your-account-token",
  target: "https://<UID>.middleware.io",
  tracer: {
    sampler: new Middleware.SessionBasedSampler({ ratio: 0.5 })
  }
});

Filter noise at the source. Use ignoreUrls for health checks and noisy third-party endpoints. Bot traffic is excluded by default through blockBotTraffic, which keeps crawlers, headless browsers, and Lighthouse runs out of your counts. Testing with Playwright and seeing nothing? That setting is why. If you’re weighing scripted checks against field data, see RUM vs. synthetic

Deduplicate in code. An error inside a polling loop can produce thousands of instances from one user. Rate-limit your own error logging before it reaches the SDK.

Alert on rate, not count. A threshold on absolute instances pages you every time traffic doubles. A threshold on error rate per session, or on affected users, tracks actual impact.

Running a Content Security Policy? Allow the SDK CDN in script-src and your ingest target in connect-src. A blocked SDK looks exactly like a healthy app: no errors at all.

React RUM implementation checklist

Use this checklist to confirm a React error monitoring setup is complete before you call it done.

  • Load the RUM script in <head>, ahead of the app bundle.
  • Set app.version at init and drive it from the build pipeline, not a hardcoded string.
  • Set env so staging data never pollutes production data.
  • Place error boundaries at route and widget seams, and report from them through Middleware.error().
  • Wire the React 19 root handlers for both uncaught and caught errors.
  • Report async and fetch failures explicitly with try/catch blocks.
  • Emit source maps in the build and upload them with the RUM account key, matching the app version exactly.
  • Strip source maps from the artifacts you serve to users.
  • Remove interpolated user data from error messages, and redact context objects before reporting them.
  • Set ignoreUrls and ignoreHeaders for any endpoint or header that carries a token.
  • Configure masking and consent settings for session replay.
  • Set tracePropagationTargets and run an APM agent in the backend.
  • Base alerts on error rate and affected users, not raw instance counts.
  • Review Core Web Vitals per route alongside error groups, not just as an app-wide average.

Why teams run React error monitoring on Middleware

Standalone error trackers stop at the browser. They tell you a component threw, then hand the investigation back to you.

“What sets their AI apart is that it does not stop at detecting issues. It actually helps fix problems in production, and for engineering teams, that’s been a real game changer.”

Nico Laqua, CEO at Corgi, whose team cut debugging and resolution time by nearly 90% with Middleware

Middleware treats a React error as one signal in a full-stack picture:

  • One platform, one timeline. Browser errors, session replay, Core Web Vitals, APM traces, logs, infrastructure metrics, and synthetics sit together, so a frontend symptom leads to a backend cause without switching tools.
  • OpenTelemetry native. OTLP is the primary ingestion path, so your instrumentation stays portable, not proprietary.
  • OpsAI does the correlation for you. Middleware’s AI SRE agent has direct access to RUM, APM, logs, and Kubernetes telemetry. It correlates a frontend error spike with the deploy or upstream failure behind it, and can open a pull request with a proposed fix.
  • Pricing that doesn’t punish visibility. Usage-based, with error detection included, so you’re not choosing between coverage and budget. See pricing for current rates.

Stop debugging from screenshots

Get RUM error tracking, session replay, and backend traces on one timeline. 14-day free trial, unlimited ingestion.

FAQs

What is React error monitoring?

React error monitoring is the continuous capture and analysis of errors thrown in a React app in production. Each error is attributed to a real user session, route, release, and line of original source code, combining browser error capture, source map resolution, session context, and correlation with backend traces.

Does an error boundary catch every React error?

No. Error boundaries catch errors thrown during rendering, in lifecycle methods, and in constructors below them. Event handlers, async code, promise rejections, server rendering, and errors thrown inside the boundary itself are not caught. Those need global handlers and explicit reporting.

Why are my React stack traces still minified in production?

Either source maps were not uploaded for that build, or the –appVersion used at upload does not match defaultAttributes[“app.version”] in your RUM init. Both values must be identical for the ORIGINAL view to resolve frames.

Does Middleware RUM track route changes in a single page application?

Yes. The SDK treats History API URL updates as new views, so React Router navigations are measured like page loads. A memory only router never updates the URL, so no new views are emitted.

Can I see Core Web Vitals and session replay together for the same error?

Yes. Every session in Middleware RUM carries Core Web Vitals for that view alongside the replay, so a slow LCP or high CLS on a route can be watched back in the same session that shows the errors on that page.

How do I stop sensitive data leaking into error reports?

Avoid interpolating user data into error messages, redact context objects before reporting them, set ignoreUrls and ignoreHeaders for token bearing requests, and use masking options so session replay scrubs sensitive fields on the client.

Can I monitor React errors without recording session video?

Yes. Set recording: “0” to collect browser traces and errors while disabling session recording. This is the common configuration for teams with strict consent requirements.

How do I connect a React error to the API call that caused it?

Add tracePropagationTargets to your RUM init and run a Middleware APM agent in your backend. Browser requests then carry trace context, and the error, the session replay, and the backend span appear on the same timeline.

Does Middleware support React Native?

Yes. Mobile RUM covers React Native, Android, iOS, and Flutter, capturing crashes, app start time, and screen load performance alongside your backend data. See the React Native setup guide.

What’s the fastest way to start?

Add the script tag, initialize with your account key, and upload source maps in your next build. Data appears on the RUM dashboard within minutes.