All posts

July 2026 · The Nohmo team

Why native crashes never reach your React Native error tracker

A React Native app is two runtimes wearing one coat. There is the JavaScript thread, where your components live, and there is the native process — Java/Kotlin on Android, Objective-C/Swift on iOS — that hosts it.

Most error trackers install a JavaScript global handler. That handler catches things thrown on the JS thread. It cannot catch a native crash, for a reason that is obvious once stated: when the native process dies, everything dies with it, including the code that would have sent the report.

This is the single most common "my error tracking is broken" report in React Native, and usually nothing is broken. The report never had a chance to be sent.

The two failure modes

A JS error. A component throws, a promise rejects unhandled. The JS thread survives, the global handler runs, the event goes into a queue and is flushed over the network. Everything works.

A native crash. A NullPointerException in a Kotlin module, a force-unwrapped nil in Swift, a SIGSEGV in a native library. The OS terminates the process immediately. There is no JS thread left to run a handler, no queue to flush, no network call. Your JS error tracker sees nothing — not an error, not even a hint that the app died.

From the dashboard's point of view, the session simply stops. Which looks exactly like a user closing the app.

What a correct setup requires

To catch native crashes you need native handlers, and they cannot report at crash time — the process is going away. They have to:

  1. Install an uncaught-exception handler on the JVM (Android) and an NSSetUncaughtExceptionHandler plus signal handlers (iOS).
  2. Write the crash to disk as the process dies. Not send it — write it. There is no time and no reliable network.
  3. Read that file on the next app launch and report it then.
  4. Attribute it back to the session it actually happened in, not the session that reported it.

Step 4 is the one most implementations skip, and it is why crash reports so often arrive detached from what the user was doing.

How to verify yours actually works

Do not trust the integration guide. Force a real native crash and confirm it appears.

A JS throw new Error() proves nothing here — that is the path that already works. You need to crash the native side:

// This is NOT a native crash — it only tests the JS handler.
throw new Error('test')

For a real test, either use a package built for it (react-native-crash-tester exposes native crash triggers for both platforms), or add a temporary native module that dereferences null on Android and calls fatalError() on iOS. Then:

  1. Build a release build. Debug builds behave differently — the RN dev tooling intercepts things release builds do not.
  2. Trigger the native crash. The app should die instantly, with no red box.
  3. Relaunch the app. This is the step people miss. The report is sent on next launch, not at crash time.
  4. Check the dashboard. If nothing arrives after a relaunch, native capture is genuinely not working.

If a crash only appears after you relaunch, that is not a bug — that is the design.

Symbolication is a separate problem

Even when the crash arrives, it may be unreadable: a stack of hex addresses rather than function names. That is symbolication, and it needs the debug symbols uploading — dSYM files on iOS, ProGuard/R8 mapping files on Android — as part of your release build.

Two independent things must be true for a native crash to be useful: it must be captured and reported on next launch, and it must be symbolicated. Teams often have one and not the other, and the failure looks similar from the dashboard.

What is still out of reach

Be aware of the boundaries of what any of this catches:

  • ANRs (Android "Application Not Responding") are not crashes. The process is alive and stuck. They need separate watchdog instrumentation.
  • NDK / C++ crashes in native libraries need a dedicated signal-handling layer beyond the JVM handler.
  • Out-of-memory kills leave almost no trace — the OS reaps the process without an exception. They are usually inferred from a missing session end, not reported.

If a tool claims to catch everything, ask which of these three it means.

Where Nohmo sits

Nohmo's React Native SDK installs the native handlers on both platforms, persists the crash to disk, and reports it on next launch attributed back to the session it happened in — so you get the screens and taps leading up to it, not just the stack.

Honest boundaries: we cover uncaught JVM exceptions on Android and Objective-C exceptions plus Swift/signal crashes on iOS. We do not cover NDK/C++ crashes or ANRs, and our stack traces are raw rather than symbolicated today — symbolication is on the roadmap. If readable native stack traces are essential right now, Sentry does that better and we would rather say so than have you find out after integrating.

Summary

  • A JS error handler cannot catch a native crash. The process is gone before it can run.
  • Native crashes must be written to disk and reported on next launch.
  • Test with a real native crash in a release build, then relaunch.
  • Capture and symbolication are separate problems — you can have one without the other.
  • ANRs, NDK crashes and OOM kills are each their own problem again.