React Native Crash Reporting: The Complete Setup Guide (2026)
React Native crash reporting is not a set-it-and-forget-it configuration. Done properly, it gives you symbolicated stack traces, session context, and network data for every crash in production. Done poorly, you get a minified stack trace with no idea what the user was doing. This guide covers everything: what to install, how to initialize it correctly, how to handle every crash type, how to upload source maps, and how to build a triage workflow that actually closes bugs.
What "Complete" Crash Reporting Means for React Native
A complete crash reporting setup captures four things:
1. JavaScript exceptions — errors thrown in React component trees and synchronous code
2. Unhandled promise rejections — async errors that were never caught
3. Native bridge crashes — exceptions from native modules crossing into JavaScript
4. Context — which screen the user was on, what network calls happened, what device/OS version
Most tools capture #1 out of the box. The gap is #2 (async errors), #3 (native crashes), and especially #4 (context). Without context, a stack trace is a puzzle. With context, it's a story you can follow.
Step 1: Install and Initialize
# npm
npm install @bugspulse/react-native
# yarn
yarn add @bugspulse/react-native
# Expo managed workflow
npx expo install @bugspulse/react-nativeFor bare React Native (iOS), link the native module:
npx pod-installInitialize BugsPulse at the very top of your entry file — before any imports that might throw:
// index.js (or index.ts)
import BugsPulse from '@bugspulse/react-native';
BugsPulse.init({
apiKey: 'bp_your_project_key',
environment: __DEV__ ? 'development' : 'production',
sessionReplay: true,
captureNetworkRequests: true,
captureUnhandledRejections: true, // async errors
});
import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('YourApp', () => App);Order matters: BugsPulse.init() must run before AppRegistry.registerComponent() or any component that might render and throw.
Step 2: Catch Every JavaScript Error
React ErrorBoundary
Without an ErrorBoundary, React Native swallows render errors in production (showing a white screen with no crash logged). Add one around your entire app:
import React from 'react';
import BugsPulse from '@bugspulse/react-native';
class AppErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ crashed: boolean }
> {
state = { crashed: false };
static getDerivedStateFromError() {
return { crashed: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
BugsPulse.captureException(error, {
extra: { componentStack: info.componentStack },
level: 'fatal',
});
}
render() {
if (this.state.crashed) {
return (
<SafeAreaView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Something went wrong. Please restart the app.</Text>
</SafeAreaView>
);
}
return this.props.children;
}
}
export default function App() {
return (
<AppErrorBoundary>
<NavigationContainer>
{/* rest of your app */}
</NavigationContainer>
</AppErrorBoundary>
);
}Unhandled Promise Rejections
BugsPulse patches the global unhandled rejection handler automatically during init(). You don't need to add anything, but you do need to ensure captureUnhandledRejections: true is set (it's the default).
To verify it's working, add a test:
// Temporary test — remove after verifying
setTimeout(() => {
Promise.reject(new Error('Test unhandled rejection'));
}, 2000);Check your BugsPulse dashboard — this error should appear within seconds.
Step 3: Track Navigation
Without navigation tracking, every crash report shows the stack trace but not which screen the user was on. With React Navigation:
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
import BugsPulse from '@bugspulse/react-native';
export default function App() {
const navRef = useNavigationContainerRef();
return (
<AppErrorBoundary>
<NavigationContainer
ref={navRef}
onReady={() => {
BugsPulse.setNavigationRef(navRef);
}}
onStateChange={() => {
const current = navRef.getCurrentRoute();
if (current?.name) {
BugsPulse.setCurrentScreen(current.name);
}
}}
>
<Stack.Navigator>
{/* ... */}
</Stack.Navigator>
</NavigationContainer>
</AppErrorBoundary>
);
}Now every crash report shows the sequence of screens the user navigated through before the crash.
Step 4: Monitor Network Requests
Network errors are a leading cause of crashes — especially null-reference errors triggered by unexpected API responses. BugsPulse captures network events automatically when captureNetworkRequests: true.
For Axios users, add the interceptor to ensure BugsPulse sees all requests:
import axios from 'axios';
import BugsPulse from '@bugspulse/react-native';
const apiClient = axios.create({ baseURL: 'https://api.example.com' });
apiClient.interceptors.response.use(
(response) => response,
(error) => {
BugsPulse.addBreadcrumb({
category: 'network',
message: `${error.config?.method?.toUpperCase()} ${error.config?.url} → ${error.response?.status}`,
level: 'error',
data: { status: error.response?.status, url: error.config?.url },
});
return Promise.reject(error);
}
);Step 5: Add Custom Context
The more context you log, the faster you can triage. Add custom attributes for key user properties:
// After authentication
BugsPulse.setUser({
id: user.id, // internal ID — not email or name
planId: user.planId, // useful for "is this a free user issue?"
});
// When entering a key flow
BugsPulse.addBreadcrumb({
message: 'Entered checkout flow',
data: { cartItemCount: cart.items.length, hasPromoCode: !!promoCode },
});
// When a feature flag is evaluated
BugsPulse.setTag('feature_new_checkout', isNewCheckoutEnabled ? 'on' : 'off');Step 6: Source Maps and Symbolication
Production JavaScript is minified and bundled. Without source maps, your stack traces look like:
TypeError: Cannot read property 'n' of undefined
at e (index.android.bundle:1:289471)
at t.t (index.android.bundle:1:289512)With source maps uploaded, the same crash becomes:
TypeError: Cannot read property 'id' of undefined
at UserProfileScreen (src/screens/UserProfileScreen.tsx:47:12)
at renderWithHooks (node_modules/react/cjs/react-dom.development.js:14985:18)Generating source maps for Android
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --sourcemap-output android/app/src/main/assets/index.android.bundle.mapGenerating source maps for iOS
npx react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/main.jsbundle --sourcemap-output ios/main.jsbundle.mapUploading to BugsPulse
npx bugspulse-cli upload-sourcemaps --api-key bp_your_project_key --version 1.2.3 --platform android --bundle android/app/src/main/assets/index.android.bundle --sourcemap android/app/src/main/assets/index.android.bundle.mapAdd this to your CI/CD pipeline as a post-build step. Source maps should be uploaded for every release build.
Expo and EAS Build
With Expo, EAS Build generates source maps automatically if you enable them in your build profile. After a successful EAS Build, download the source maps from the EAS dashboard or use the EAS CLI:
eas build:list --status finished --limit 1
# Get the build ID from the output
eas build:inspect --id <build-id> --stage artifactsThen upload with the BugsPulse CLI as above.
Step 7: Set Up Alerts
Configure alerts so you don't need to check the dashboard manually:
- New crash — alert when a crash type appears for the first time in this release
- Crash spike — alert when a crash rate exceeds 1% or increases by 50% vs last week
- High-impact crash — alert when a single crash affects more than 100 unique users
In BugsPulse, alerts are configured per project under Settings → Alerts. Connect Slack or email for instant notifications.
The Production Triage Workflow
Once crashes are flowing, here's how to triage them efficiently every week:
1. Monday morning review: Sort all open crashes by unique users affected in the last 7 days. Focus on the top 5.
2. For each crash: Open 5 sessions that ended in that crash. Look for: common navigation paths, network errors that preceded the crash, shared device model or OS version.
3. Reproduce: Armed with session context (device model, OS version, exact navigation path, network state), attempt to reproduce locally. If you can't reproduce, add more breadcrumbs around the suspected area.
4. Fix and tag: Fix the crash and tag it as "Resolved" in BugsPulse with the app version where it was fixed. The crash reporter will automatically reopen it if it appears in later versions.
5. Verify: After the fix ships, monitor the crash rate for that issue for 48 hours. If it goes to zero across 100+ sessions, it's genuinely fixed.
Common Mistakes
Not initializing before other imports: If any code in your app's dependency tree throws during module evaluation, BugsPulse won't have captured it. Always put BugsPulse.init() as the very first thing.
Not uploading source maps on every release: A common pattern is to upload source maps once during setup, then forget. Your builds change every release. Automate this in CI/CD.
Ignoring unhandled rejections: These are silent until the app enters a broken state. Enable captureUnhandledRejections and check your dashboard for them — there are usually more than you expect.
Not setting user context: Anonymous crashes are much harder to debug than crashes with a user ID, plan type, or other identifying context. Even a numeric user ID (not email) dramatically speeds up triage.
Summary
Complete React Native crash reporting requires five pieces: initialization before any other code, an ErrorBoundary for render errors, automatic unhandled rejection capture, navigation tracking for screen context, and source map upload for readable stack traces. Get all five right and your crash dashboard becomes your most effective debugging tool — not just a list of incomprehensible stack traces.