
React Native Error Tracking in Production
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',
captureUnhandledRejections: true,
captureNetworkRequests: true,
sessionReplay: 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:
// App.tsx
import { BugsPulseErrorBoundary } from '@bugspulse/react-native';
export default function App() {
return (
<BugsPulseErrorBoundary>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</BugsPulseErrorBoundary>
);
}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, {
context: { userId, operation: 'loadUserProfile' },
level: 'warning',
});
return null; // graceful fallback
}
}Adding Breadcrumbs and Context
Stack traces alone rarely tell the full story. What was the user doing? What was the network state? Was the app in the foreground or background? Adding structured context transforms an opaque crash into a reproducible bug report.
// Set user context after authentication
function onAuthSuccess(user: User) {
BugsPulse.setUser({ id: user.id, plan: user.planId });
BugsPulse.setTag('user_type', user.isEnterprise ? 'enterprise' : 'individual');
}
// Add breadcrumbs at critical state transitions
function onCheckoutStart(cart: Cart) {
BugsPulse.addBreadcrumb({
message: 'Checkout started',
data: { itemCount: cart.items.length, total: cart.total },
});
}
// Clear user context on logout
function onLogout() {
BugsPulse.clearUser();
}Breadcrumbs 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)
Upload source maps as part of your CI/CD pipeline for every release build:
# iOS
npx bugspulse-cli upload-sourcemaps \
--api-key $BUGSPULSE_API_KEY \
--version $APP_VERSION \
--platform ios \
--bundle ios/main.jsbundle \
--sourcemap ios/main.jsbundle.map
# Android
npx bugspulse-cli upload-sourcemaps \
--api-key $BUGSPULSE_API_KEY \
--version $APP_VERSION \
--platform android \
--bundle android/app/src/main/assets/index.android.bundle \
--sourcemap android/app/src/main/assets/index.android.bundle.mapAccording to a 2025 survey by industry analysts, teams that upload source maps resolve production crashes 3x faster than those that rely on raw stack traces Mobile DevOps Report, 2025. Make source map upload a blocking step in your release pipeline — never ship to production without it.
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:
- Monday morning: Review new error groups from the weekend (highest activity period for most consumer apps)
- Classify each new error: fatal crash, non-fatal exception, or known issue
- Assign the top 3 by user impact to the current sprint
- Tag recurring errors with the existing GitHub issue number
- 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.
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 beforeAppRegistry.registerComponent() - Error boundary wrapping the entire app component tree
-
captureUnhandledRejections: truein initialization config - Source maps uploaded for both iOS and Android release builds
- User context set after authentication (anonymized user ID)
- Breadcrumbs added 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
- The Complete Guide to React Native Crash Debugging
- React Native Crash Reporting: The Complete Setup Guide (2026)
- React Native Network Request Monitoring: A Complete Guide
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