Fix crash in async void TurboModule methods when NSException is thrown#55390
Fix crash in async void TurboModule methods when NSException is thrown#55390Luxlorys wants to merge 1 commit intofacebook:mainfrom
Conversation
When an async void TurboModule method throws an NSException (e.g., during app termination or rapid UI transitions), the exception is caught and converted to a JSError which is then rethrown. However, since this code executes inside an async dispatch block via invokeAsync(), nothing in the call stack can catch the C++ exception, causing immediate app termination with SIGABRT/EXC_BAD_ACCESS. This is particularly common during: - Rapid screen transitions while closing the app - Focusing an input (triggering keyboard) then quickly closing the app - Any scenario where native modules are deallocated while async void methods are still executing The fix replaces the throw with RCTLogError, which preserves error visibility for debugging while preventing the crash. This is safe because: - Void methods have no return value to the caller - Async dispatch means the original JS call already returned - There's no Promise to reject This is consistent with the existing handling in performMethodInvocation, which already has special handling for async exceptions (it re-throws NSException instead of converting to JSError for async calls). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Hi @Luxlorys! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
| // Log the error for debugging purposes instead of crashing. | ||
| RCTLogError( | ||
| @"Exception thrown while invoking async method %s.%s: %@", | ||
| moduleName, | ||
| methodNameStr.c_str(), | ||
| exception); |
There was a problem hiding this comment.
@Luxlorys, let's handle errors in async methods consistently.
Is there a strong reason for us to do something different in these changed lines vs here:
There was a problem hiding this comment.
Lines 399-405 (Promise-based async methods) can re-throw NSException because they execute within the createPromise() wrapper infrastructure (lines 253-360).
When the exception is re-thrown at line 404, it's caught by the Promise wrapper which can then reject the Promise and communicate the error back to JavaScript
through the resolve/reject blocks.
Async void methods execute through a fundamentally different code path:
- They call performVoidMethodInvocation() which dispatches directly via invokeAsync() to a background queue
- There's no Promise wrapper, no resolve/reject blocks, no error communication channel
- The JS call has already returned undefined before the async work executes
- Any exception thrown (whether C++ or NSException) has nothing to catch it on the dispatch queue
If we used the same approach as line 404 (@throw exception;) for void methods, the app would still crash because there's no infrastructure to catch the
re-thrown NSException in the dispatch block's execution context.
Correct me if I missed something, I'm new here
There was a problem hiding this comment.
@RSNara Hey, check message above when you have time, please.
There was a problem hiding this comment.
Ah! So you're saying that we should be throwing in the promise case.
|
Hey Reviewers, I know you're busy. But the last update from your side has been more than 2 weeks ago. This is affecting a lot of users, especially on the newArch. Can we bump up the priority and take a look at this asap? considering you guys recommend everyone switch to this architecture and have basically strong armed pretty much everyone running expo onto this. @RSNara, if you are unable to be more quick about this, could you please tag some more people who can take a look at this! That would be great. |
| // Log the error for debugging purposes instead of crashing. | ||
| RCTLogError( | ||
| @"Exception thrown while invoking async method %s.%s: %@", | ||
| moduleName, | ||
| methodNameStr.c_str(), | ||
| exception); |
There was a problem hiding this comment.
Ah! So you're saying that we should be throwing in the promise case.
Build 24 still crashed with the same SIGABRT in ObjCTurboModule::performVoidMethodInvocation — SDK 54 / RN 0.81 has New Architecture ON by default (since RN 0.76). Two fixes applied: 1. Set "newArchEnabled": false in app.json to disable TurboModules 2. Patch RCTTurboModule.mm (from facebook/react-native#55390) to replace throw with RCTLogError in async void method catch block Verified: 0 occurrences of RCT_NEW_ARCH_ENABLED in generated pods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Should this be closed in favor of #56265? |
Summary
Fixes uncaught C++ exception crash in
performVoidMethodInvocationwhen async void TurboModule methods throwNSExceptionduring app termination or rapid UI transitions.Problem
In
RCTTurboModule.mm, when an async void TurboModule method throws anNSException, the exception is caught and converted to aJSErrorwhich is then rethrown:Since this code executes inside an async dispatch block via
invokeAsync(), nothing in the call stack can catch the C++ exception, causing immediate app termination withSIGABRT/EXC_BAD_ACCESS.How to Reproduce
Common culprits include search/Spotlight indexing modules, analytics modules, and keyboard-related modules that fire async void methods during app lifecycle transitions.
Solution
Replace the
throwwithRCTLogErrorto log the error instead of crashing:Why This Fix is Safe
RCTLogErrorperformMethodInvocationalready has special handling for async exceptions (re-throwsNSExceptioninstead of converting toJSError)Related Issues
performMethodInvocation, butperformVoidMethodInvocationwas not addressedChangelog
[IOS] [FIXED] - Fix crash when async void TurboModule methods throw NSException
Test Plan
voidFuncThrowsinRCTSampleTurboModule.mm)