The Complete Guide to React Native Crash Debugging
Crashes are the fastest way to lose users. A 1% crash rate sounds small until you do the math — on an app with 100,000 daily active users, that's 1,000 crashes every single day. Unlike web apps where you can open DevTools and reproduce issues immediately, mobile crashes demand deliberate instrumentation to diagnose and fix.
This guide walks through the complete React Native crash debugging workflow: the types of crashes you'll encounter, how to capture them properly, how to read stack traces, and how to use session context to understand exactly what drove your user into a crash state.
The 5 Types of React Native Crashes
Not all crashes are equal. Understanding the type tells you where to look first.
1. JavaScript exceptions
The most common type. An unhandled TypeError, ReferenceError, or a thrown error that wasn't caught somewhere in your component tree. React Native's default behavior is to show a red screen in development and a white screen (or silent crash) in production.
// Classic example — accessing a property on undefined
const userName = user.profile.name; // crashes if user or profile is null2. Unhandled promise rejections
Async code that throws without a .catch() or try/catch around an await. These are silent killers — they don't always crash the app immediately, but they leave it in a broken state.
// Missing error handling
async function loadData() {
const response = await fetch('/api/data'); // throws on network error
return response.json();
}3. Native module bridge crashes
When a native module (written in Java/Kotlin or Objective-C/Swift) throws an exception that crosses the bridge, React Native can't recover gracefully. These show up as cryptic native stack traces with no JavaScript context.
4. Out-of-memory (OOM) kills
Android and iOS will kill your process when it uses too much memory. There's no exception thrown and no stack trace — the process simply dies. Common causes: large image lists without recycling, memory leaks from uncleared intervals, and holding references in closures.
5. ANR (Application Not Responding) on Android
When the main thread is blocked for more than 5 seconds, Android kills the app with an ANR. Caused by synchronous work on the main thread — file I/O, heavy computation, or blocking network calls.
Why Crashes Are So Hard to Debug
Android fragmentation
Android runs on thousands of device/OS combinations. A crash on a Samsung Galaxy A12 running Android 10 with a custom OEM memory manager may never reproduce on your Pixel 7 running Android 14. Without crash data from production devices, you're guessing.
Minified stack traces
Production JavaScript bundles are minified. A raw crash report looks like:
TypeError: Cannot read property 'n' of undefined
at e (index.android.bundle:1:923847)
at t (index.android.bundle:1:923901)
at r (index.android.bundle:1:924012)Without source maps, these are useless. You need to upload your source maps to your crash reporter at build time.
Missing context
A stack trace tells you where the crash happened, not why. The why is in what happened before: which screen was the user on? Did a previous API call return a 500? Was the app in the background? Without that context, you're staring at a line number with no story around it.
Setting Up Crash Reporting in React Native
Install the SDK
npm install @bugspulse/react-native
# or
yarn add @bugspulse/react-nativeFor bare React Native, link the native module:
npx pod-installInitialize early in your app
Call BugsPulse.init() at the very top of your entry file, before any other imports that might throw:
// index.js or App.tsx
import BugsPulse from '@bugspulse/react-native';
BugsPulse.init({
apiKey: 'bp_your_project_key',
environment: __DEV__ ? 'development' : 'production',
sessionReplay: true,
captureNetworkRequests: true,
});Add an ErrorBoundary for JS crashes
React's ErrorBoundary catches render-phase errors and lets you log them before showing a fallback UI:
import React from 'react';
import BugsPulse from '@bugspulse/react-native';
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
BugsPulse.captureException(error, {
componentStack: info.componentStack,
});
}
render() {
if (this.state.hasError) {
return <FallbackScreen />;
}
return this.props.children;
}
}
// Wrap your entire app
export default function App() {
return (
<ErrorBoundary>
<NavigationContainer>
{/* ... */}
</NavigationContainer>
</ErrorBoundary>
);
}Capture unhandled promise rejections
// In your init block, after BugsPulse.init()
const originalHandler = global.Promise;
// BugsPulse automatically patches Promise — just make sure init runs firstBugsPulse's SDK hooks into the global unhandled rejection handler automatically during init(). No manual wiring needed.
Reading a React Native Crash Report
Once crashes are flowing into your dashboard, here's how to triage them efficiently.
The crash fingerprint
A good crash reporter groups crashes by a normalized fingerprint — the stack trace with memory addresses stripped. This lets you see "TypeError: Cannot read property 'id' of undefined in UserProfileScreen" happening 47 times across 23 users, rather than 47 individual reports.
Prioritize crashes by user impact (unique users affected), not raw count. One crash happening 500 times to the same user is less urgent than a crash happening 30 times across 30 different users.
What to look for in session context
Before jumping to the stack trace, look at what the user was doing:
- Last 3 screens — was the crash always preceded by a specific navigation?
- Last network request — did a 401, 500, or timeout happen right before?
- Device and OS — is this crash isolated to Android 9 or Samsung devices?
- App version — was this introduced in the last release?
- Session duration — does it crash after 5 seconds (startup crash) or after 15 minutes (memory leak)?
Symbolicated vs raw stack traces
If your stack traces look like minified garbage, you need to upload source maps. For React Native:
# Generate the bundle and source map
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.mapUpload the .map file to your crash reporter along with the build version. BugsPulse automatically symbolicates incoming stack traces against the uploaded map.
Debugging Android-Specific Crashes
Null pointer exceptions from native modules
A common pattern: a React Native component mounts, calls a native module method, and the native module crashes because a required Android permission wasn't granted.
java.lang.NullPointerException: Attempt to invoke virtual method
on a null object reference at com.example.CameraModule.openCameraFix: Always check permissions before calling native modules. Use PermissionsAndroid.check() before camera/location/contacts access.
Android 12+ background activity restrictions
Apps targeting Android 12+ can't start activities from the background. If you're triggering navigation from a push notification handler, you'll hit a BackgroundActivityStartNotAllowed exception.
Fix: Use PendingIntent with the correct flags and ensure your deep link handler runs on the main thread.
Different behavior across React Native versions
React Native's new architecture (Fabric + TurboModules) changed how native crashes propagate. If you're on RN 0.71+, native module exceptions are now wrapped differently. Check your crash reporter for exceptions with TurboModule in the trace.
Debugging iOS-Specific Crashes
Memory warnings before OOM
iOS sends applicationDidReceiveMemoryWarning before killing the process. You can log a breadcrumb when this fires to confirm OOM kills in your crash reports:
import { AppState } from 'react-native';
import BugsPulse from '@bugspulse/react-native';
// In your App component
useEffect(() => {
const sub = AppState.addEventListener('memoryWarning', () => {
BugsPulse.addBreadcrumb({
message: 'Memory warning received',
level: 'warning',
});
});
return () => sub.remove();
}, []);EXC_BAD_ACCESS crashes
These are usually use-after-free bugs in native code — a native object that was deallocated while a JavaScript reference still existed. They're rare with modern React Native but can occur in older native modules.
If you see EXC_BAD_ACCESS (SIGSEGV) or EXC_BAD_ACCESS (SIGBUS) in your crash reports, check for:
- Native modules manually managing memory
- Accessing
selfin an Objective-C block after the owner was deallocated - Race conditions in native thread callbacks
The Debugging Workflow
1. Triage by impact
Each week, sort crashes by unique users affected. Fix the top 3. Crashes affecting 1 user on an obscure device can wait.
2. Group by session path
Look at 5–10 sessions that ended in the same crash. If 8 out of 10 crashed after navigating to CheckoutScreen following a network error on /cart, that's your bug — the crash isn't in CheckoutScreen, it's in how you handle the error state returned by /cart.
3. Reproduce locally with the session data
Armed with: device model, OS version, app state, and the sequence of events — you can now attempt to reproduce. Set your test device to the same OS version, navigate the same path, and trigger the same network condition (use a proxy like Charles or mitmproxy to inject errors).
4. Fix and verify with a canary release
Deploy the fix to a small percentage of users first (5–10%). Monitor your crash dashboard for 24 hours. If the crash rate for that cohort drops to zero, you've confirmed the fix. If it persists, the root cause is somewhere else.
Common Crash Patterns and Quick Fixes
| Crash | Likely Cause | Fix |
|---|---|---|
Cannot read property 'X' of undefined | Missing null check | Optional chaining: obj?.prop?.x |
Maximum update depth exceeded | Infinite render loop | Check useEffect dependency arrays |
VirtualizedList: You have a large list | Missing keyExtractor | Add stable keyExtractor prop |
Text strings must be rendered within a <Text> | Conditional whitespace in JSX | Wrap all text in <Text> components |
| White screen on launch (production only) | Uncaught error before ErrorBoundary | Move BugsPulse.init() to the very first line |
| Crash after background/foreground cycle | AppState not cleaned up | Remove listeners in useEffect cleanup |
Summary
React Native crash debugging comes down to three things: capture crashes with enough context before they become patterns, symbolicate your stack traces so they're readable, and use session data to reconstruct the exact story of what the user was doing. A raw stack trace tells you where the code failed. The session tells you why.
Set up instrumentation once, and your crashes become a prioritized backlog instead of a mystery.