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

React Native Error Tracking in Production: A Complete Guide

NFNourin Mahfuj Finick··10 min read

Error tracking in production React Native is different from debugging in development. In development you have a Metro bundler, a red screen with a stack trace, and a hot reload. In production you have minified bundles, no dev tools, and users who just see a white screen or an app that silently crashes. Getting useful data from production requires deliberate instrumentation.


This guide covers the complete setup for tracking every error category in a production React Native app.


The Four Error Categories


1. Handled exceptions (errors you catch)


Errors you catch in try/catch blocks or Promise .catch() handlers. The app doesn't crash, but the error still represents a failure that users experience.


2. Unhandled JavaScript exceptions


Errors thrown in synchronous JS code that aren't caught anywhere. In production, React Native catches these at the JS-to-native boundary and either calls the global error handler or terminates the JS context.


3. Unhandled promise rejections


Async errors that escape all .catch() handlers and await try/catch blocks. Silent until they cause unexpected app state or a later crash.


4. Native crashes (Java/ObjC/Swift)


Exceptions in the native layer — device-specific, permission-related, or native module bugs. These show as SIGABRT, SIGSEGV, or Java exceptions in crash logs.


Setup: Capture All Four Categories


// index.js — MUST be the very first code that runs
import BugsPulse from '@bugspulse/react-native';

BugsPulse.init({
  apiKey: process.env.BUGSPULSE_KEY,
  environment: __DEV__ ? 'development' : 'production',
  sessionReplay: true,
  captureNetworkRequests: true,
  captureUnhandledRejections: true, // Category 3
});

import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('MyApp', () => App);

// App.tsx — wrap with ErrorBoundary for Category 2
import { BugsPulseErrorBoundary } from '@bugspulse/react-native';

export default function App() {
  return (
    <BugsPulseErrorBoundary>
      <NavigationContainer>
        <RootNavigator />
      </NavigationContainer>
    </BugsPulseErrorBoundary>
  );
}

Categories 1 and 4 need additional setup (manual capture and source maps respectively).


Capturing Handled Exceptions


Errors you catch gracefully should still be reported — you want to know how often they happen and whether they're increasing:


// API call that fails gracefully
async function loadUserProfile(userId: string) {
  try {
    return await api.getProfile(userId);
  } catch (error) {
    // Report it — even though we handled it
    BugsPulse.captureException(error as Error, {
      context: { userId, operation: 'loadUserProfile' },
      level: 'warning',
    });
    return null; // graceful fallback
  }
}

// Form validation error
function validatePaymentForm(data: PaymentData) {
  if (!data.cardNumber || data.cardNumber.length < 16) {
    const error = new Error('Invalid card number format');
    BugsPulse.captureException(error, { level: 'info' });
    return false;
  }
  return true;
}

Use severity levels to distinguish severity: fatal for crashes, error for recoverable failures, warning for degraded states, info for notable events.


JavaScript Exception Handling Patterns


Global error handler


import { ErrorUtils } from 'react-native';

// Override the global error handler for any JS error that escapes
const defaultHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
  BugsPulse.captureException(error, { level: isFatal ? 'fatal' : 'error' });
  defaultHandler(error, isFatal);
});

Per-component error handling


class ScreenErrorBoundary extends React.Component {
  componentDidCatch(error: Error, info: React.ErrorInfo) {
    BugsPulse.captureException(error, {
      context: {
        screen: this.props.screenName,
        componentStack: info.componentStack,
      },
    });
  }
  // ...
}

// Use per-screen boundaries for finer-grained triage
<ScreenErrorBoundary screenName="CheckoutScreen">
  <CheckoutScreen />
</ScreenErrorBoundary>

Tracking Unhandled Promise Rejections


BugsPulse captures these automatically when captureUnhandledRejections: true. To verify your async code isn't creating silent rejections:


// Patterns that create unhandled rejections:

// WRONG — fire-and-forget with no error handling
loadData(); // if this rejects, nothing catches it

// WRONG — .then() without .catch()
api.fetchUser().then(setUser);

// CORRECT
loadData().catch(error => BugsPulse.captureException(error));

api.fetchUser()
  .then(setUser)
  .catch(error => {
    BugsPulse.captureException(error);
    showErrorToast('Failed to load user');
  });

Adding Context to Every Error


Context transforms a stack trace into a debuggable story:


// Set user context early in the app lifecycle
async function onAuthSuccess(user: User) {
  BugsPulse.setUser({
    id: user.id,
    plan: user.planId,
  });

  BugsPulse.setTag('user_type', user.isEnterprise ? 'enterprise' : 'individual');
  BugsPulse.setTag('locale', user.locale);
}

// Add breadcrumbs at key state transitions
function onCheckoutStart(cart: Cart) {
  BugsPulse.addBreadcrumb({
    message: 'Checkout started',
    data: {
      itemCount: cart.items.length,
      hasPromoCode: !!cart.promoCode,
    },
  });
}

// Clear user on logout
function onLogout() {
  BugsPulse.clearUser();
}

Source Maps: Making Stack Traces Readable


Without source maps, production errors look like:


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

With source maps:


TypeError: Cannot read property 'id' of undefined
  at UserProfileScreen (src/screens/UserProfileScreen.tsx:47:12)

Upload source maps in your CI/CD pipeline for every release:


# After building
npx bugspulse-cli upload-sourcemaps   --api-key $BUGSPULSE_KEY   --version $APP_VERSION   --platform android   --bundle android/app/src/main/assets/index.android.bundle   --sourcemap android/app/src/main/assets/index.android.bundle.map

Error Grouping and Triage


Once errors flow in, group and prioritize them:


By frequency — How many times per day? Trend up or down since last release?


By unique users — How many distinct users are affected? Prioritize by user impact, not raw count.


By recency — Is this a new error (introduced in the last release) or an old one?


By crash type — Fatal crashes (app terminated) vs. handled errors (degraded state) get different urgency levels.


By platform — iOS only, Android only, or cross-platform? The platform distribution narrows the root cause search significantly.


Production Error Tracking Checklist


□ BugsPulse.init() called before AppRegistry.registerComponent()
□ ErrorBoundary wrapping the entire app
□ captureUnhandledRejections: true in config
□ Source maps uploaded for every release build (iOS + Android)
□ User context set after authentication
□ Custom breadcrumbs at key user flows (checkout, auth, data sync)
□ Alert configured for new error types and crash rate spikes
□ Weekly triage process: sort by user impact, fix top 3

React Native error tracking in production is not a one-time setup — it's an ongoing practice. The teams with the lowest crash rates instrument well, triage weekly, and treat their error dashboard as a first-class product signal rather than a post-incident tool.