AI-powered crash analysis is now available on all plans — including Free.Read the crash analysis guide

React Native Error Tracking in Production

NFNourin Mahfuj Finick··

React Native apps crash in production for reasons you will never see in development: minified bundles with mangled variable names, device-specific native module bugs across 24,000+ Android models, Hermes engine JIT quirks, and promise rejections that silently swallow errors. The Metro bundler's red screen of death is a development luxury — in production, a single uncaught JavaScript exception can leave users staring at a white screen with no feedback loop back to you. Setting up proper error tracking across all four categories of production failures is the difference between knowing your app crashed and knowing exactly why.

The Four Categories of Production Errors

Production error tracking in React Native requires capturing four distinct error types, each with its own mechanism and tooling.

1. Handled Exceptions

Errors you explicitly catch in try/catch blocks or .catch() handlers. The app doesn't crash, but the error still represents a user-facing failure. Handled exceptions are the most common error category and also the most under-reported — many teams catch errors and log them to the console, which is invisible in production.

2. Unhandled JavaScript Exceptions

Errors thrown in synchronous code that escape all try/catch blocks. In production builds, React Native catches these at the JavaScript-to-native boundary. With Hermes enabled (the default since React Native 0.74), unhandled exceptions trigger the global error handler, but the stack trace is minified and the error message is often truncated. Without source maps, a stack trace like TypeError: Cannot read property 'n' of undefined at e (index.android.bundle:1:289471) is useless.

3. Unhandled Promise Rejections

Async errors that escape all .catch() handlers and await try/catch blocks. These are the silent killers of React Native production apps. An API call that fails inside a useEffect with no .catch() turns into an unhandled rejection that degrades app state without crashing. The user sees stale data or a spinning loader, but there is no way to distinguish this from a network issue without a proper error tracking setup.

4. Native Crashes

Crashes in the Java/Kotlin (Android) or Objective-C/Swift (iOS) layers. These come from native module bugs, device-specific permission issues, or memory pressure from the JavaScript heap. Native crashes show up as SIGABRT, SIGSEGV, or Java NullPointerException in device logs. React Native cannot catch or recover from them — they require native symbolication and full crash context.

Complete Error Tracking Setup

The most reliable pattern is to initialize your error tracker before anything else in your app. This ensures that errors occurring during module initialization or early component mounting are captured:

// index.js — must be the first import
import BugsPulse from '@bugspulse/react-native';
 
BugsPulse.init({
  apiKey: process.env.BUGSPULSE_API_KEY,
  environment: __DEV__ ? 'development' : 'production',
  captureNetworkRequests: true,
});
 
import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('MyApp', () => App);

Next, wrap your application root with an error boundary to catch rendering exceptions before they reach the native layer. React Native has no built-in error boundary component, so this is a standard React class component that reports to BugsPulse in componentDidCatch:

// ErrorBoundary.tsx
import React from 'react';
import BugsPulse from '@bugspulse/react-native';
 
export class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
  state = { hasError: false };
 
  static getDerivedStateFromError() {
    return { hasError: true };
  }
 
  componentDidCatch(error: Error) {
    BugsPulse.captureException(error, { boundary: 'root' });
  }
 
  render() {
    if (this.state.hasError) return null; // or a fallback UI
    return this.props.children;
  }
}

For handled exceptions, use explicit capture calls with context. A common mistake is catching an error and doing nothing with it — that error represents a real user experience failure that should be visible in your dashboard:

async function loadUserProfile(userId: string) {
  try {
    return await api.getProfile(userId);
  } catch (error) {
    BugsPulse.captureException(error as Error, {
      userId,
      operation: 'loadUserProfile',
    });
    return null; // graceful fallback
  }
}

Adding User Context and Custom Events

Stack traces alone rarely tell the full story. What was the user doing? What operation was in flight? Adding structured context transforms an opaque crash into a reproducible bug report.

// Set the current user after authentication
function onAuthSuccess(user: User) {
  BugsPulse.setUser(user.id);
}
 
// Track custom events at critical state transitions
function onCheckoutStart(cart: Cart) {
  BugsPulse.track('checkout_started', {
    itemCount: cart.items.length,
    total: cart.total,
  });
}

Custom events are particularly valuable because they create a timeline of user actions leading up to a crash. When combined with event-based session replay, you can reconstruct the exact sequence of taps, navigation events, and network requests that preceded the failure — without recording the user's screen or storing any personal data.

Source Maps: Making Stack Traces Readable

Production React Native bundles are minified by Metro, which renames variables and collapses function names into single letters. A raw production stack trace looks like this:

TypeError: Cannot read property 'n' of undefined
  at e (index.android.bundle:1:289471)
  at t (index.android.bundle:1:178234)
  at r (index.android.bundle:1:45210)

Source maps reverse this minification, so you see the original source location:

TypeError: Cannot read property 'id' of undefined
  at UserProfileScreen (src/screens/UserProfileScreen.tsx:47:12)
  at renderWithHooks (react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js:3456:18)

Metro generates a .map file alongside every release bundle (index.android.bundle.map, main.jsbundle.map). Keep those files from every release you ship — without the matching source map for a given build, a minified stack trace like the one above is effectively unreadable. Treat generating and archiving source maps as a blocking step in your release pipeline, not an afterthought.

Error Triage and Prioritization

Once errors are flowing into your dashboard, the next challenge is triage. Not every error deserves the same urgency. A structured triage workflow prevents alert fatigue and ensures critical bugs get fixed first.

By user impact. An error affecting 1,000 users is more urgent than one affecting 10, regardless of the error count. BugsPulse automatically deduplicates errors by fingerprint and shows the unique user count for each group.

By recency. New errors introduced in the latest release deserve immediate attention. Old, stable errors that affect a small number of users may represent edge cases that are acceptable to defer. The React Native Crash Debugging Guide covers this triage workflow in more detail.

By severity. Fatal crashes (app termination) are always higher priority than non-fatal handled exceptions. But don't ignore non-fatal errors entirely — a warning-level error that affects 30% of your users represents a degraded experience that will drive uninstalls over time.

A practical weekly triage cadence looks like this:

  1. Monday morning: Review new error groups from the weekend (highest activity period for most consumer apps)
  2. Classify each new error: fatal crash, non-fatal exception, or known issue
  3. Assign the top 3 by user impact to the current sprint
  4. Tag recurring errors with the existing GitHub issue number
  5. Check that source maps are uploaded for the latest release

Common Production Error Patterns

Some error patterns appear so frequently in production React Native apps that they deserve special attention:

Null checks on deeply nested objects. The most common production crash. A user profile screen reads user.preferences.theme but preferences is null because the API returned an incomplete response. Optional chaining (user?.preferences?.theme) eliminates this class of crash entirely.

Hermes engine edge cases. Hermes handles JavaScript execution differently from V8 or JSC. Certain JavaScript patterns — particularly around Proxy objects, Symbol.toStringTag, and large destructuring operations — can cause Hermes-specific crashes that don't appear in iOS development builds or Metro's development mode Meta Hermes Compatibility Notes, 2025.

Memory pressure on low-end Android devices. React Native's JavaScript heap can grow rapidly when rendering large FlatLists, processing images at full resolution, or storing excessive data in Redux. On devices with 2-3GB of RAM, this leads to native OOM (out-of-memory) crashes that appear as SIGKILL in crash logs. The React Native Memory Leak Detection guide covers profiling and fixing these patterns.

What About react-native-exception-handler?

react-native-exception-handler is a well-known open-source package (unrelated to any specific vendor) that gives you two low-level hooks: setJSExceptionHandler for uncaught JavaScript exceptions and setNativeExceptionHandler for native Java/Objective-C crashes. It's useful when you want to intercept an exception before the app's default handling (e.g. to show a custom "restart the app" screen) or to route errors into a fully custom logging pipeline.

On its own, it's a low-level primitive, not a full error-tracking solution: it hands you the raw error, but you still need to build the dashboard, deduplication, source-map symbolication, and alerting on top of it. Most teams either use it purely for the custom-UI hook and still forward the error to a dedicated tracker, or skip it entirely in favor of a global handler that's already wired up, like BugsPulse's init() and error boundary above.

import { setJSExceptionHandler } from 'react-native-exception-handler';
import BugsPulse from '@bugspulse/react-native';
 
setJSExceptionHandler((error, isFatal) => {
  BugsPulse.captureException(error, { isFatal: String(isFatal) });
}, true);

Privacy-First Error Tracking

Mobile privacy regulations — GDPR, CCPA, HIPAA, and Apple's App Store privacy labels — increasingly restrict what error tracking tools can capture. Traditional crash reporters collect device IDs, IP addresses, and raw user data by default, which creates compliance risk.

Bugspulse was built with a zero-PII architecture from the ground up. No device identifiers, no IP addresses, no user IDs are stored on the server. Device state is captured through anonymized fingerprints that cannot be traced back to individual users. Session replay is event-based (taps, navigation, network events) rather than video-based, so no screen content is recorded.

This architecture means you get full production debugging capability — complete stack traces, session context, breadcrumbs, and device state — without the legal overhead of data processing agreements, consent banners, or privacy impact assessments for every SDK update. Try Bugspulse free to see the complete error tracking setup in action.

Production Error Tracking Checklist

Use this checklist when preparing a new release:

  • BugsPulse.init() called before AppRegistry.registerComponent()
  • Error boundary wrapping the entire app component tree
  • Source maps archived for both iOS and Android release builds
  • User context set after authentication (anonymized user ID)
  • Custom events tracked at key user flows (checkout, auth, data sync, navigation)
  • Alert configured for new error types and crash rate spikes
  • Weekly triage process established: prioritize by user impact, fix top 3

Error tracking in production React Native is not a one-time setup — it is an ongoing practice that evolves with every release, every new device model, and every user behavior pattern. The teams with the lowest crash rates are not the ones with the most sophisticated debugging tools; they are the ones who treat their error dashboard as a first-class product signal and triage it with the same discipline as feature development.

Related reading

Try BugsPulse free — event-based session replay, AI-powered crash analysis, and native crash capture for React Native and Flutter. 500 sessions/month, no credit card required. Get started at bugspulse.com