Skip to content

Runtime Exceptions in TurboModule Void Methods Crash the App #53960

Open

Description

@scarpenter

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).

Reproducer Project

Steps to reproduce

  1. Clone the reproducer project
  2. 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

  1. react-native-bot commented on Sep 26, 2025

    @react-native-bot
    Collaborator

    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.

  2. scarpenter commented on Sep 26, 2025

    @scarpenter
    Author

    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

  3. cortinico commented on Sep 26, 2025

    @cortinico
    Contributor

    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: picassomd/turbo-modules-exceptions

    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.

  4. removed
    Needs: ReproThis issue could be improved with a clear list of steps to reproduce the issue.
    on Sep 26, 2025
  5. Thotaharshavardhani commented on Oct 10, 2025

    @Thotaharshavardhani

    I was able to reproduce the issue on my end.

  6. Luxlorys commented on Feb 2, 2026

    @Luxlorys

    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.

      "dependencies": {
        "@datadog/mobile-react-native": "^2.14.1",
        "@datadog/mobile-react-native-session-replay": "^2.14.1",
        "@datadog/mobile-react-navigation": "^2.14.1",
        "@gorhom/bottom-sheet": "5.2.8",
        "@hookform/resolvers": "3.10.0",
        "@invertase/react-native-apple-authentication": "^2.4.0",
        "@legendapp/state": "3.0.0-beta.43",
        "@notifee/react-native": "^9.1.8",
        "@react-native-community/netinfo": "^11.4.1",
        "@react-native-firebase/app": "^23.0.0",
        "@react-native-firebase/messaging": "^23.0.0",
        "@react-native-google-signin/google-signin": "^13.0.0",
        "@react-native/new-app-screen": "0.81.4",
        "@react-navigation/bottom-tabs": "^7.4.0",
        "@react-navigation/native": "7.1.28",
        "@react-navigation/native-stack": "7.9.0",
        "@react-navigation/stack": "7.6.13",
        "@shopify/react-native-skia": "^2.4.0",
        "@tanstack/react-query": "5.64.2",
        "@tryvital/vital-core-react-native": "^5.4.2",
        "@tryvital/vital-devices-react-native": "^5.4.2",
        "@tryvital/vital-health-react-native": "^5.4.2",
        "axios": "1.7.9",
        "dayjs": "1.11.13",
        "i18next": "23.15.1",
        "immer": "10.1.1",
        "lottie-react-native": "^7.3.0",
        "patch-package": "8.0.0",
        "react": "19.1.0",
        "react-hook-form": "7.55.0",
        "react-i18next": "15.0.2",
        "react-native": "0.81.4",
        "react-native-animated-numbers": "^0.6.3",
        "react-native-appsflyer": "^6.17.8",
        "react-native-background-timer": "^2.4.1",
        "react-native-bootsplash": "6.3.2",
        "react-native-calendars": "^1.1309.0",
        "react-native-chat-sdk": "1.13.0",
        "react-native-config": "1.5.3",
        "react-native-date-picker": "^5.0.10",
        "react-native-device-info": "14.0.2",
        "react-native-edge-to-edge": "^1.7.0",
        "react-native-fbsdk-next": "^13.4.1",
        "react-native-gesture-handler": "2.28.0",
        "react-native-image-picker": "^8.2.1",
        "react-native-keyboard-controller": "1.18.5",
        "react-native-linear-gradient": "^2.8.3",
        "react-native-mmkv": "^4.1.1",
        "react-native-modalfy": "3.5.5",
        "react-native-nitro-modules": "^0.33.2",
        "react-native-otp-entry": "1.8.2",
        "react-native-pager-view": "8.0.0",
        "react-native-permissions": "^5.4.2",
        "react-native-reanimated": "4.1.1",
        "react-native-safe-area-context": "^5.5.2",
        "react-native-screens": "4.16.0",
        "react-native-svg": "15.15.1",
        "react-native-tab-view": "4.2.2",
        "react-native-toast-message": "^2.3.0",
        "react-native-turbo-image": "1.22.1",
        "react-native-unistyles": "2.20.0",
        "react-native-vector-icons": "10.2.0",
        "react-native-version-check": "^3.4.5",
        "react-native-webview": "13.16.0",
        "react-native-worklets": "0.5.1",
        "react-timer-hook": "3.0.8",
        "socket.io-client": "^4.8.1",
        "ts-pattern": "5.6.0",
        "zod": "3.24.1",
        "zustand": "5.0.10"
      },
      "devDependencies": {
        "@babel/core": "^7.25.2",
        "@babel/preset-env": "^7.25.3",
        "@babel/runtime": "^7.25.0",
        "@commitlint/cli": "^19.5.0",
        "@commitlint/config-conventional": "^19.5.0",
        "@react-native-community/cli": "20.0.0",
        "@react-native-community/cli-platform-android": "20.0.0",
        "@react-native-community/cli-platform-ios": "20.0.0",
        "@react-native/babel-preset": "0.81.4",
        "@react-native/eslint-config": "0.81.4",
        "@react-native/metro-config": "0.81.4",
        "@react-native/typescript-config": "0.81.4",
        "@svgr/cli": "^8.1.0",
        "@types/jest": "^29.5.13",
        "@types/react": "^19.0.0",
        "@types/react-native-background-timer": "^2.0.0",
        "@types/react-native-vector-icons": "^6.4.18",
        "@types/react-native-version-check": "^3.4.5",
        "@types/react-test-renderer": "^19.1.0",
        "@typescript-eslint/eslint-plugin": "^8.8.1",
        "@typescript-eslint/parser": "^8.8.1",
        "babel-jest": "^29.6.3",
        "babel-plugin-module-resolver": "^5.0.2",
        "eslint": "8.57.0",
        "eslint-config-airbnb": "19.0.4",
        "eslint-config-prettier": "9.0.0",
        "eslint-import-resolver-typescript": "3.6.1",
        "eslint-plugin-boundaries": "^4.2.2",
        "eslint-plugin-import": "2.29.1",
        "eslint-plugin-jsx-a11y": "6.8.0",
        "eslint-plugin-prettier": "5.1.3",
        "eslint-plugin-react": "7.34.0",
        "eslint-plugin-react-hooks": "4.6.0",
        "eslint-plugin-react-native": "4.1.0",
        "husky": "9.0.11",
        "metro-react-native-babel-preset": "^0.77.0",
        "prettier": "3.0.3",
        "react-native-svg-transformer": "^1.5.0",
        "reactotron-react-native": "^5.1.9",
        "reactotron-react-native-mmkv": "^0.2.7",
        "reactotron-react-query": "^1.0.4",
        "ts-to-zod": "3.8.5",
        "typescript": "^5.8.3"
      },
      "engines": {
        "node": ">=20"
      },
    
  7. Luxlorys commented on Feb 2, 2026

    @Luxlorys

    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.

      "dependencies": {
        "@datadog/mobile-react-native": "^2.14.1",
        "@datadog/mobile-react-native-session-replay": "^2.14.1",
        "@datadog/mobile-react-navigation": "^2.14.1",
        "@gorhom/bottom-sheet": "5.2.8",
        "@hookform/resolvers": "3.10.0",
        "@invertase/react-native-apple-authentication": "^2.4.0",
        "@legendapp/state": "3.0.0-beta.43",
        "@notifee/react-native": "^9.1.8",
        "@react-native-community/netinfo": "^11.4.1",
        "@react-native-firebase/app": "^23.0.0",
        "@react-native-firebase/messaging": "^23.0.0",
        "@react-native-google-signin/google-signin": "^13.0.0",
        "@react-native/new-app-screen": "0.81.4",
        "@react-navigation/bottom-tabs": "^7.4.0",
        "@react-navigation/native": "7.1.28",
        "@react-navigation/native-stack": "7.9.0",
        "@react-navigation/stack": "7.6.13",
        "@shopify/react-native-skia": "^2.4.0",
        "@tanstack/react-query": "5.64.2",
        "@tryvital/vital-core-react-native": "^5.4.2",
        "@tryvital/vital-devices-react-native": "^5.4.2",
        "@tryvital/vital-health-react-native": "^5.4.2",
        "axios": "1.7.9",
        "dayjs": "1.11.13",
        "i18next": "23.15.1",
        "immer": "10.1.1",
        "lottie-react-native": "^7.3.0",
        "patch-package": "8.0.0",
        "react": "19.1.0",
        "react-hook-form": "7.55.0",
        "react-i18next": "15.0.2",
        "react-native": "0.81.4",
        "react-native-animated-numbers": "^0.6.3",
        "react-native-appsflyer": "^6.17.8",
        "react-native-background-timer": "^2.4.1",
        "react-native-bootsplash": "6.3.2",
        "react-native-calendars": "^1.1309.0",
        "react-native-chat-sdk": "1.13.0",
        "react-native-config": "1.5.3",
        "react-native-date-picker": "^5.0.10",
        "react-native-device-info": "14.0.2",
        "react-native-edge-to-edge": "^1.7.0",
        "react-native-fbsdk-next": "^13.4.1",
        "react-native-gesture-handler": "2.28.0",
        "react-native-image-picker": "^8.2.1",
        "react-native-keyboard-controller": "1.18.5",
        "react-native-linear-gradient": "^2.8.3",
        "react-native-mmkv": "^4.1.1",
        "react-native-modalfy": "3.5.5",
        "react-native-nitro-modules": "^0.33.2",
        "react-native-otp-entry": "1.8.2",
        "react-native-pager-view": "8.0.0",
        "react-native-permissions": "^5.4.2",
        "react-native-reanimated": "4.1.1",
        "react-native-safe-area-context": "^5.5.2",
        "react-native-screens": "4.16.0",
        "react-native-svg": "15.15.1",
        "react-native-tab-view": "4.2.2",
        "react-native-toast-message": "^2.3.0",
        "react-native-turbo-image": "1.22.1",
        "react-native-unistyles": "2.20.0",
        "react-native-vector-icons": "10.2.0",
        "react-native-version-check": "^3.4.5",
        "react-native-webview": "13.16.0",
        "react-native-worklets": "0.5.1",
        "react-timer-hook": "3.0.8",
        "socket.io-client": "^4.8.1",
        "ts-pattern": "5.6.0",
        "zod": "3.24.1",
        "zustand": "5.0.10"
      },
      "devDependencies": {
        "@babel/core": "^7.25.2",
        "@babel/preset-env": "^7.25.3",
        "@babel/runtime": "^7.25.0",
        "@commitlint/cli": "^19.5.0",
        "@commitlint/config-conventional": "^19.5.0",
        "@react-native-community/cli": "20.0.0",
        "@react-native-community/cli-platform-android": "20.0.0",
        "@react-native-community/cli-platform-ios": "20.0.0",
        "@react-native/babel-preset": "0.81.4",
        "@react-native/eslint-config": "0.81.4",
        "@react-native/metro-config": "0.81.4",
        "@react-native/typescript-config": "0.81.4",
        "@svgr/cli": "^8.1.0",
        "@types/jest": "^29.5.13",
        "@types/react": "^19.0.0",
        "@types/react-native-background-timer": "^2.0.0",
        "@types/react-native-vector-icons": "^6.4.18",
        "@types/react-native-version-check": "^3.4.5",
        "@types/react-test-renderer": "^19.1.0",
        "@typescript-eslint/eslint-plugin": "^8.8.1",
        "@typescript-eslint/parser": "^8.8.1",
        "babel-jest": "^29.6.3",
        "babel-plugin-module-resolver": "^5.0.2",
        "eslint": "8.57.0",
        "eslint-config-airbnb": "19.0.4",
        "eslint-config-prettier": "9.0.0",
        "eslint-import-resolver-typescript": "3.6.1",
        "eslint-plugin-boundaries": "^4.2.2",
        "eslint-plugin-import": "2.29.1",
        "eslint-plugin-jsx-a11y": "6.8.0",
        "eslint-plugin-prettier": "5.1.3",
        "eslint-plugin-react": "7.34.0",
        "eslint-plugin-react-hooks": "4.6.0",
        "eslint-plugin-react-native": "4.1.0",
        "husky": "9.0.11",
        "metro-react-native-babel-preset": "^0.77.0",
        "prettier": "3.0.3",
        "react-native-svg-transformer": "^1.5.0",
        "reactotron-react-native": "^5.1.9",
        "reactotron-react-native-mmkv": "^0.2.7",
        "reactotron-react-query": "^1.0.4",
        "ts-to-zod": "3.8.5",
        "typescript": "^5.8.3"
      },
      "engines": {
        "node": ">=20"
      },
    

    Patched react native itself by removing 'throw convertNSExceptionToJSError'

  8. aramb-dev commented on Feb 28, 2026

    @aramb-dev

    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 — convertNSExceptionToJSError successfully enters the Hermes runtime from the dispatch queue and corrupts the heap. We captured 3 crashes from the same launch session:

    1. Dispatch queue thread crashes in BasedPointer::operator bool() inside HiddenClass::findProperty, called from convertNSExceptionToJSError (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.

    2. JS thread crashes in StringPrimitive::getStringLength() during Array.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.

    3. HadesGC thread crashes in HiddenClass::initializeMissingPropertyMap with a corrupted pointer (PAC auth failure at 0xffff0001065c9410). Meanwhile another dispatch queue thread is in convertNSArrayToJSIArray → convertNSExceptionToJSError — same performVoidMethodInvocation path.

    All three are the same root cause: the @catch in performVoidMethodInvocation calls convertNSExceptionToJSError(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: false eliminates it since it removes TurboModules entirely.

  9. georgeshchegolev commented on Sep 13, 2026

    @georgeshchegolev

    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):

    } @catch (NSException *exception) {
      if (isSync) {
        // We can only convert NSException to JSError in sync method calls.
        // See https://github.com/reactwg/react-native-new-architecture/discussions/276#discussioncomment-12567155
        throw convertNSExceptionToJSError(runtime, exception, std::string{moduleName}, methodNameStr);
      } else {
        @throw exception;
      }
    }

    performVoidMethodInvocation (0.79.6, line 440):

    } @catch (NSException *exception) {
      throw convertNSExceptionToJSError(runtime, exception, std::string{moduleName}, methodNameStr);
    }

    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 own methodQueue gets). EXC_BAD_ACCESS, KERN_INVALID_ADDRESS at 0x0:

    Thread 15 CRASHED — com.meta.react.turbomodulemanager.queue
    0  hermes  HadesGC::EvacAcceptor<false>::acceptNullable(PinnedHermesValue&)
    1  hermes  GCScope::mark(RootAcceptor&)
    2  hermes  HandleRootOwner::markGCScopes(RootAcceptor&)
    3  hermes  Runtime::markRoots(RootAndSlotAcceptorWithNames&, bool)
    4  hermes  HadesGC::youngGenEvacuateImpl<HadesGC::EvacAcceptor<false>>(...)
    5  hermes  HadesGC::youngGenCollection(std::string, bool)
    6  hermes  HadesGC::allocSlow(unsigned int)
    7  hermes  GCBase::makeAVariable<DynamicStringPrimitive<char,false>, ...>(...)
    8  hermes  StringPrimitive::createEfficientImpl<char>(...)
    9  hermes  HermesRuntimeImpl::stringHVFromAscii(char const*, unsigned long)
    10 hermes  HermesRuntimeImpl::createStringFromAscii(char const*, unsigned long)
    11 App     jsi::Object::setProperty<char const*>(Runtime&, String const&, char const*&&)  jsi-inl.h:127
    12 App     jsi::Object::setProperty<char const*>(Runtime&, char const*, char const*&&)    jsi-inl.h:119
    13 App     TurboModuleConvertUtils::convertNSExceptionToJSError(...)                      RCTTurboModule.mm:225
    14 App     invocation function for block in ObjCTurboModule::performVoidMethodInvocation(...)
    15 App     std::__func<...performVoidMethodInvocation...::$_1...>::operator()()           function.h:319
    16 libdispatch  _dispatch_call_block_and_release
    

    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. createStringFromAscii hits allocSlow, which triggers a young-gen collection on the module queue, which then marks roots from GCScopes the JS thread owns. At that moment the JS thread was mid-allocation and blocked waiting for the GC lock:

    Thread 3 — com.facebook.react.runtime.JavaScript
    3  libc++  std::recursive_mutex::lock()
    4  hermes  HadesGC::pauseBackgroundTask()
    5  hermes  GCBase::makeA<HiddenClass, ...>(...)
    6  hermes  HiddenClass::copyToNewDictionary(...)
    7  hermes  HiddenClass::deleteProperty(...)
    8  hermes  JSObject::deleteComputed(...)
    9  hermes  Interpreter::interpretFunction<false, false>(...)
    ...
    13 hermes  Runtime::drainJobs()
    14 hermes  HermesRuntimeImpl::drainMicrotasks(int)
    15 App     RuntimeScheduler_Modern::performMicrotaskCheckpoint(...)  RuntimeScheduler_Modern.cpp:423
    

    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 exception branch (matching performMethodInvocation) 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 a patch-package patch 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions