Runtime Exceptions in TurboModule Void Methods Crash the App #53960
Description
Description
When raising a runtime exception (RuntimeException on Android, NSException on iOS) in a TurboModule the behavior is different based on whether the method in the TurboModule has a return type or is declared as void.
If the method that raises the exception has a return type, the exception is visible in the Javascript code that called the method. The error can be handled in a catch statement and if it's not then the standard error screen is shown in development.
If the method that raises the exception is declared as void, the behavior is slightly different between iOS and Android. On iOS, the app immediately crashes (if a debugger is attached, the cause is often EXC_BAD_ACCESS). On Android, the standard error screen is shown but then the app stops responding (the UI goes white and the app is frozen).
This reproducer project has a TurboModule with two methods: nonVoidFunction and voidFunction. Each is implemented to raise a runtime exception. There are two buttons on the main screen, one to call each function. Tapping the "Non Void Function" button calls the corresponding function and the standard error screen is displayed. After dismissing or minimizing the error, the app continues to function. Tapping the "Void Function" button exhibits the behavior described above (white screen on Android, crash on iOS).
Steps to reproduce
- Clone the reproducer project
- Run the application and tap the "Void Function" button
React Native Version
0.81.4
Affected Platforms
Runtime - Android, Runtime - iOS
Areas
TurboModule - The New Native Module System
Output of npx @react-native-community/cli info
System:
OS: macOS 15.7
CPU: (12) arm64 Apple M4 Pro
Memory: 125.47 MB / 24.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.14.0
path: ~/.nvm/versions/node/v22.14.0/bin/node
Yarn:
version: 1.22.22
path: ~/.nvm/versions/node/v22.14.0/bin/yarn
npm:
version: 10.9.2
path: ~/.nvm/versions/node/v22.14.0/bin/npm
Watchman:
version: 2024.12.02.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods: Not Found
SDKs:
iOS SDK:
Platforms:
- DriverKit 24.5
- iOS 18.5
- macOS 15.5
- tvOS 18.5
- visionOS 2.5
- watchOS 11.5
Android SDK: Not Found
IDEs:
Android Studio: 2025.1 AI-251.25410.109.2511.13752376
Xcode:
version: 16.4/16F6
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.14
path: /usr/bin/javac
Ruby:
version: 2.7.2
path: /Users/sean/.asdf/shims/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.0.0
wanted: 20.0.0
react:
installed: 19.1.0
wanted: 19.1.0
react-native:
installed: 0.81.4
wanted: 0.81.4
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
Not available. See description and reproduction steps above.
MANDATORY Reproducer
https://github.com/picassomd/turbo-modules-exceptions
Screenshots and Videos
No response
Activity
react-native-bot commentedon Sep 26, 2025
Warning
Missing reproducer: We could not detect a reproducible example in your issue report. Reproducers are mandatory and we can accept only one of those as a valid reproducer:
You can read more about about it on our website: How to report a bug.
scarpenter commentedon Sep 26, 2025
I'm not sure why this is marked as "Missing reproducer". The Reproducer project is linked twice in the description and is included in the MANDATORY Reproducer section.
Here it is again: https://github.com/picassomd/turbo-modules-exceptions
cortinico commentedon Sep 26, 2025
Because the reproducer needs to be a repo under your user account (@scarpenter) and not another account (@picassomd) as otherwise folks will just add random repositories.
Anyway this is valid repro, thanks for providing it.
Thotaharshavardhani commentedon Oct 10, 2025
I was able to reproduce the issue on my end.
Luxlorys commentedon Feb 2, 2026
Same problem on my side. Upgraded React Native from 0.76.7 to 0.81.5, already patched 3 libraries because i receive
0x00000001010230d4 in std::__1::__function::__func<facebook::react::ObjCTurboModule::performVoidMethodInvocation(facebook::jsi::Runtime&, char const*, NSInvocation*, NSMutableArray*)::$_1, std::__1::allocator<facebook::react::ObjCTurboModule::performVoidMethodInvocation(facebook::jsi::Runtime&, char const*, NSInvocation*, NSMutableArray*)::$_1>, void ()>::operator()() ()error during app termination. Usually i need to open an app -> search through the app -> quickly close the app -> app is closed -> i see 'app crashed' alert.Luxlorys commentedon Feb 2, 2026
Patched react native itself by removing 'throw convertNSExceptionToJSError'
aramb-dev commentedon Feb 28, 2026
Hitting this in our Expo 54 / RN 0.81.5 app on iOS 18.6.2. Release builds via TestFlight, New Architecture enabled. Crashes 0.2-0.3s after launch, 100% repro with a heavy initial render.
Our variant is SIGSEGV instead of SIGABRT —
convertNSExceptionToJSErrorsuccessfully enters the Hermes runtime from the dispatch queue and corrupts the heap. We captured 3 crashes from the same launch session:Dispatch queue thread crashes in
BasedPointer::operator bool()insideHiddenClass::findProperty, called fromconvertNSExceptionToJSError(RCTTurboModule.mm:220) →performVoidMethodInvocation(RCTTurboModule.mm:438). This is the direct thread safety violation — JSI property writes happening on a dispatch worker thread while the JS thread is active.JS thread crashes in
StringPrimitive::getStringLength()duringArray.map→Array.join(component tree rendering). Null deref at address 0x4 — the GC moved or collected the object while the dispatch queue thread was concurrently writing to the heap.HadesGC thread crashes in
HiddenClass::initializeMissingPropertyMapwith a corrupted pointer (PAC auth failure at 0xffff0001065c9410). Meanwhile another dispatch queue thread is inconvertNSArrayToJSIArray→convertNSExceptionToJSError— sameperformVoidMethodInvocationpath.All three are the same root cause: the
@catchinperformVoidMethodInvocationcallsconvertNSExceptionToJSError(runtime, ...)on a dispatch queue, which writes to the Hermes heap from the wrong thread. For async void methods there's no caller to receive the error anyway, so this should either be scheduled back to the JS thread or just logged without touching JSI.Workaround:
newArchEnabled: falseeliminates it since it removes TurboModules entirely.fix(1.6.1): remove SplashScreen JS lifecycle (iOS 26 + A18 Pro NSExce…
fix(1.6.1): disable New Architecture (iOS 26 + A18 Pro TurboModule cr…
feat(1.6.3): hyper-fast cold start + lazy navigators
Merge claude/kind-hertz-forward — 1.6.3 cold start optimization
Fix iPadOS 26 launch crash: patch AsyncStorage NSFileManager exceptions
fix: Patch RN TurboModule crash + properly re-enable Sign in with Apple
georgeshchegolev commentedon Sep 13, 2026
Another SIGSEGV variant, on RN 0.79.6 / iOS 26.6.2, from a production TestFlight build. Adding it because the version spread matters — the neighbouring issue #54859 frames this as iOS-26-specific, and combined with @aramb-dev's 0.81.5/iOS 18.6.2 report above this clearly spans 0.79 → 0.81 and iOS 18 → 26.
The part I haven't seen stated in this thread: the guard for this already exists in the same file, and the void path just didn't get it.
performMethodInvocation(0.79.6, line 369):performVoidMethodInvocation(0.79.6, line 440):It reads like a missed case rather than an intentional difference: the void function already has the equivalent flag,
shouldVoidMethodsExecuteSync_, in scope and reads it three times elsewhere in the same body — including immediately above and below this@catch.Symbolicated stack. The faulting thread is the shared module queue (
com.meta.react.turbomodulemanager.queue, i.e._sharedModuleQueue, which every module without its ownmethodQueuegets).EXC_BAD_ACCESS,KERN_INVALID_ADDRESS at 0x0:Line 225 is
cause.setProperty(runtime, "name", [exception.name UTF8String]), and the frame order (119 → 127 → Hermes) puts the fault in allocating the value string, not the key.One mechanism detail worth adding to the three cases above: here the module queue doesn't merely write to the heap, it runs the collector.
createStringFromAsciihitsallocSlow, which triggers a young-gen collection on the module queue, which then marks roots fromGCScopes the JS thread owns. At that moment the JS thread was mid-allocation and blocked waiting for the GC lock:So the GC mutex is doing what it can, but the collection itself is being driven from a thread that holds none of the runtime's invariants.
A practical note for anyone hitting this: because the conversion crashes inside Hermes, the report never names the module or method that raised. Restoring the
@throw exceptionbranch (matchingperformMethodInvocation) doesn't stop the module from raising, but it turns an undiagnosable GC segfault into an uncaught NSException whose report identifies the culprit — which is how you find the actual offender. We're running that as apatch-packagepatch against 0.79.6 and it behaves as expected.#55390 is open and takes the log-and-swallow approach instead. Either shape fixes the memory corruption; happy to see whichever lands.
iOS çökmesi: void TurboModule NSException'ı JS'e çevrilmesin (patch-p…