React Native native module crash native bridge debugging requires a different toolkit than pure JavaScript errors. When your app crashes at the boundary between your JavaScript code and native iOS or Android modules, the stack trace often disappears into compiled native code, and your JS error handler never fires. This guide shows you how to locate these crashes, decode native stack traces, and understand where the bridge itself is breaking.
The native bridge is React Native's most powerful feature—and its most fragile. Every time your JavaScript calls a native module (camera, file system, sensors), data crosses a serialization boundary where type mismatches and memory issues hide. When that bridge breaks, your app crashes before the JS layer can catch it.
The Native Bridge Problem
React Native communicates with native code via a bridge that serializes JavaScript values to native types and back. This works seamlessly most of the time, but crashes happen when:
- A native module receives invalid data types (passing a string where an int is expected)
- Native memory is exhausted or corrupted
- A background thread in native code throws an uncaught exception
- Native module cleanup fails, leaving dangling pointers
- Race conditions between JS and native threads
Unlike how-to-debug-production-errors, native crashes don't trigger JavaScript error boundaries. Your app simply terminates, and the system writes the crash to the native log (Logcat on Android, system.log on iOS). The JavaScript side never sees it.
Understanding Native Module Crash Types
Native crashes fall into two categories: crashes in your own native modules and crashes in third-party native dependencies. Both produce different debugging patterns.
Your Custom Native Modules
When you write a bridge module in Objective-C or Java, unhandled exceptions in native code kill the process:
// Android native module (Java)
@ReactMethod
public void uploadFile(String path, Promise promise) {
try {
File file = new File(path);
// If path is null, this throws NullPointerException
// and crashes the app immediately
byte[] data = Files.readAllBytes(file.toPath());
// ...
} catch (IOException e) {
promise.reject("UPLOAD_ERROR", e.getMessage());
}
}
In iOS, the equivalent is an uncaught Objective-C exception:
RCT_EXPORT_METHOD(uploadFile:(NSString *)path
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
@try {
NSData *fileData = [NSData dataWithContentsOfFile:path];
if (!fileData) {
reject(@"FILE_ERROR", @"Could not read file", nil);
return;
}
// Process fileData...
} @catch (NSException *e) {
reject(@"NATIVE_ERROR", e.reason, nil);
}
}
Third-Party Native Library Crashes
Libraries you didn't write (Realm, SQLite, camera) may crash if used incorrectly or hit edge cases. These crashes are harder to debug because you don't have the source.
Debugging Android Native Crashes with Logcat
When a React Native app crashes on Android, the native exception is logged in Logcat before the app terminates. Your first step is to watch Logcat while reproducing the crash.
Connect your device or emulator and run:
adb logcat | grep -i "FATAL\|EXCEPTION\|AndroidRuntime\|crash"
Or filter more granularly:
adb logcat AndroidRuntime:E *:S
A typical native crash in Logcat looks like:
11-15 14:32:18.456 1234 1234 E AndroidRuntime: FATAL EXCEPTION: main
11-15 14:32:18.456 1234 1234 E AndroidRuntime: Process: com.myapp, PID: 1234
11-15 14:32:18.456 1234 1234 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'int java.nio.ByteBuffer.capacity()' on a null object reference
11-15 14:32:18.456 1234 1234 E AndroidRuntime: at com.myapp.MyNativeModule.processBuffer(MyNativeModule.java:42)
11-15 14:32:18.456 1234 1234 E AndroidRuntime: at com.facebook.react.bridge.NativeModule.invoke(NativeModule.java:120)
The key fields are the exception type (NullPointerException), the message, and the stack trace lines. Each line shows a method and its line number. Open the file and check line 42 in MyNativeModule.java—that's where the null was dereferenced.
If you have ProGuard or R8 enabled, the stack trace will show obfuscated class names (like a$b.c). To decode it, use the mapping file: bundletool deobfuscate --bundle=app.aab --mapping=mapping.txt < crash.txt. Without deobfuscation, you're reading gibberish.
For non-Java native crashes (C/C++ code in a .so library), Logcat logs the signal and memory address:
11-15 14:32:18.456 1234 1234 A libc : Signal 11 (SIGSEGV), code 1
11-15 14:32:18.456 1234 1234 A libc : Segmentation fault
A segmentation fault means your native code tried to read or write an invalid memory address. This often happens with buffer overflows or use-after-free bugs. To decode the exact line, you need symbols. Run:
adb shell "cat /proc/version" # Verify device architecture
ndk-stack -sym <your-app>/lib/arm64-v8a < logcat.txt
The ndk-stack tool uses your NDK's symbol files to map memory addresses back to function names and line numbers.
Debugging iOS Native Crashes with Xcode
On iOS, native crashes appear in Xcode's console when running from Xcode, or in the device's system log if the app crashes in the background or on a real device:
log stream --predicate 'process == "YourApp"' --level debug
A typical iOS native crash shows:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Crashed Thread: 0
Thread 0 Crashed:
0 YourFramework 0x00010bc4 -[MyNativeModule processData:] + 120
1 YourFramework 0x00010a88 -[MyNativeModule exposed_processData:resolve:reject:] + 68
2 com.facebook.react 0x000245ff invokeCallback + 512
The memory address 0x0000000000000000 is a null pointer dereference. Open Xcode's Debug navigator, find your framework in the dSYM file, and search for the method processData: at offset 120 to find the exact line.
To symbolicate a crash without Xcode, use:
atos -o YourApp.app.dSYM/Contents/Resources/DWARF/YourApp \
-l 0x10000000 0x00010bc4
Replace 0x10000000 with the load address and 0x00010bc4 with the crashed address from the log.
If your app crashes in production on iOS, Apple's crash reporter sends symbolicated crashes to App Store Connect. Download them and how-to-read-a-stack-trace to find the line. Without dSYM files, you'll see only hex addresses—useless without symbols.
Error Propagation and the JavaScript Layer
Some native crashes don't crash the app immediately. Instead, the native module catches the error and rejects the Promise, which JavaScript handles:
// JavaScript side
import { NativeModules } from 'react-native';
const MyModule = NativeModules.MyModule;
MyModule.riskyOperation()
.then(() => console.log('Success'))
.catch((error) => {
console.log('Native error:', error.message);
// Error handling here—no crash
});
But if the native module doesn't catch the exception, it propagates up the bridge and crashes. The crash log will show the RCTNativeModule is involved.
To reliably catch native errors, always wrap your Promise callbacks:
try {
const result = await MyModule.riskyOperation();
console.log(result);
} catch (error) {
console.error('Operation failed:', error);
// Report to LightTrace or another error tracker
}
And always reject Promises in your native module, never throw:
@ReactMethod
public void riskyOperation(Promise promise) {
try {
// Do work
promise.resolve(result);
} catch (Exception e) {
promise.reject("ERROR_CODE", e.getMessage()); // ✓ Safe
// Never: throw new RuntimeException(e); ✗ Crash
}
}
Best Practices for Stable Native Bridges
- Validate inputs at the bridge. Null-check everything that comes from JavaScript. The bridge doesn't enforce types.
- Use try-catch in native modules. Every public native method should catch and reject, never throw.
- Test on real devices. Emulators sometimes mask memory issues that crash on real hardware.
- Monitor native crashes. Use error tracking (like error-tracking-best-practices) that captures both JS and native crashes. LightTrace captures native crashes from Sentry SDKs when configured.
- Keep dependencies up to date. Third-party native libraries often patch crashes in minor releases.
- Defer heavy work to background threads. Don't block the main thread in native code; it will trigger android-anr-outofmemoryerror on Android.
LightTrace integrates with the Sentry SDK for React Native, which captures native crashes automatically. Point your Sentry initialization at LightTrace's endpoint, and all crashes—JavaScript, native, and bridge—will be fingerprinted and de-duplicated in your dashboard.
Debugging Bridge Serialization Issues
Some crashes happen because the bridge can't serialize the data you're trying to pass. For example, passing a large object with circular references:
const obj = { name: 'test' };
obj.self = obj; // Circular reference
MyModule.sendData(obj); // Bridge fails to serialize
The bridge only serializes primitives, arrays, and plain objects. Before passing anything across the bridge, verify it's JSON-serializable:
try {
JSON.stringify(data);
MyModule.sendData(data); // Safe
} catch (e) {
console.error('Data not serializable:', e);
}
Native crashes from bridge serialization are often cryptic because they happen in the bridge itself, not your code. Look for JSBridge or RCTBridge in the stack trace—that's the clue.
Native bridge crashes are rare if you follow these patterns, but when they happen, methodically check Logcat or the system log first, find the line number, and ask: Is this a null pointer? A type mismatch? A threading issue? Answering that question is 90% of the fix.
Start tracking errors in minutes
Start capturing native crashes in your React Native app—sign up free on LightTrace to see your first native stack traces in minutes, fully symbolicated and grouped by issue.