feat(tm-ios): Reject Promise if Turbo Module method throws an Error
Summary:
Android change here
This PR builds upon the previous work done in https://github.com/facebook/react-native/pull/36925, which introduced native stack traces to the JSError for synchronous functions.
The current modifications concentrate on functions that return Promises. Prior to this PR, errors within Promise-returning functions would be thrown at the platform layer crashing the app without a link to the JS stack.
After the implementation of this PR, errors thrown within Promise-returning functions are now captured and transformed into rejected Promises. These rejected Promises contain a JS Error object that contains both the JS stack trace and the cause, along with the platform stack trace.
Additionally, this PR ensures that rejections from native functions are now linked to the JS stack trace, providing a more comprehensive view of the rejection flow.
Changelog:
[GENERAL][ADDED] - Turbo Modules Promise-returning functions reject with JS and platform stack traces information
Test Plan:
| iOS |
|---|
Example of intentionally rejected promise on iOS:
{
"name": "Error",
"message": "Exception in HostFunction: intentional promise rejection",
"stack": "@[native code]\nrejectPromise@http://localhost:8081/js/examples/TurboModule/SampleTurboModuleExample.bundle:42:70\nonPress@http://localhost:8081/js/examples/TurboModule/SampleTurboModuleExample.bundle:242:71\n_performTransitionSideEffects@http://localhost:8081/js/RNTesterApp.ios.bundle:52614:22\n_receiveSignal@http://localhost:8081/js/RNTesterApp.ios.bundle:52570:45\nonResponderRelease@http://localhost:8081/js/RNTesterApp.ios.bundle:52433:34\ninvokeGuardedCallbackProd@http://localhost:8081/js/RNTesterApp.ios.bundle:3068:21\ninvokeGuardedCallback@http://localhost:8081/js/RNTesterApp.ios.bundle:3154:42\ninvokeGuardedCallbackAndCatchFirstError@http://localhost:8081/js/RNTesterApp.ios.bundle:3157:36\nexecuteDispatch@http://localhost:8081/js/RNTesterApp.ios.bundle:3221:48\nexecuteDispatchesInOrder@http://localhost:8081/js/RNTesterApp.ios.bundle:3238:26\nexecuteDispatchesAndRelease@http://localhost:8081/js/RNTesterApp.ios.bundle:4962:35\nforEach@[native code]\nforEachAccumulated@http://localhost:8081/js/RNTesterApp.ios.bundle:3680:22\nrunEventsInBatch@http://localhost:8081/js/RNTesterApp.ios.bundle:4980:27\nrunExtractedPluginEventsInBatch@http://localhost:8081/js/RNTesterApp.ios.bundle:5002:25\n@http://localhost:8081/js/RNTesterApp.ios.bundle:5020:42\nbatchedUpdates$1@http://localhost:8081/js/RNTesterApp.ios.bundle:14856:20\nbatchedUpdates@http://localhost:8081/js/RNTesterApp.ios.bundle:4951:36\ndispatchEvent@http://localhost:8081/js/RNTesterApp.ios.bundle:5013:23",
"cause": {
"code": "code_1",
"stackSymbols": [
"0 RNTester 0x00000001031f9958 RCTJSErrorFromCodeMessageAndNSError + 112",
"1 RNTester 0x00000001034cecc8 ___ZZN8facebook5react15ObjCTurboModule13createPromiseERNS_3jsi7RuntimeENSt3__112basic_stringIcNS5_11char_traitsIcEENS5_9allocatorIcEEEEU13block_pointerFvU13block_pointerFvP11objc_objectEU13block_pointerFvP8NSStringSH_P7NSErrorEU13block_pointerFvP11NSExceptionEEENK3$_0clES4_RKNS2_5ValueEPSU_m_block_invoke.66 + 96",
"2 RNTester 0x000000010374e774 -[RCTSampleTurboModule getValueWithPromise:resolve:reject:] + 232",
"3 CoreFoundation 0x000000018043d6c0 __invoking___ + 144",
"4 CoreFoundation 0x000000018043aa44 -[NSInvocation invoke] + 276",
"5 CoreFoundation 0x000000018043acdc -[NSInvocation invokeWithTarget:] + 60",
"6 RNTester 0x00000001034c4de4 ___ZN8facebook5react15ObjCTurboModule23performMethodInvocationERNS_3jsi7RuntimeEbPKcP12NSInvocationP14NSMutableArrayU13block_pointerFvP11NSExceptionE_block_invoke + 240",
"7 RNTester 0x00000001034d5120 _ZZN8facebook5react15ObjCTurboModule23performMethodInvocationERNS_3jsi7RuntimeEbPKcP12NSInvocationP14NSMutableArrayU13block_pointerFvP11NSExceptionEENK3$_1clEv + 32",
"8 RNTester 0x00000001034d50f4 _ZNSt3__18__invokeB6v15006IRZN8facebook5react15ObjCTurboModule23performMethodInvocationERNS1_3jsi7RuntimeEbPKcP12NSInvocationP14NSMutableArrayU13block_pointerFvP11NSExceptionEE3$_1JEEEDTclclsr3stdE7declvalIT_EEspclsr3stdE7declvalIT0_EEEEOSJ_DpOSK_ + 24",
"9 RNTester 0x00000001034d50ac _ZNSt3__128__invoke_void_return_wrapperIvLb1EE6__callIJRZN8facebook5react15ObjCTurboModule23performMethodInvocationERNS3_3jsi7RuntimeEbPKcP12NSInvocationP14NSMutableArrayU13block_pointerFvP11NSExceptionEE3$_1EEEvDpOT_ + 24",
"10 RNTester 0x00000001034d5088 _ZNSt3__110__function12__alloc_funcIZN8facebook5react15ObjCTurboModule23performMethodInvocationERNS2_3jsi7RuntimeEbPKcP12NSInvocationP14NSMutableArrayU13block_pointerFvP11NSExceptionEE3$_1NS_9allocatorISI_EEFvvEEclB6v15006Ev + 28",
"11 RNTester 0x00000001034d3ce0 _ZNSt3__110__function6__funcIZN8facebook5react15ObjCTurboModule23performMethodInvocationERNS2_3jsi7RuntimeEbPKcP12NSInvocationP14NSMutableArrayU13block_pointerFvP11NSExceptionEE3$_1NS_9allocatorISI_EEFvvEEclEv + 28",
"12 RNTester 0x000000010308d65c _ZNKSt3__110__function12__value_funcIFvvEEclB6v15006Ev + 68",
"13 RNTester 0x000000010308d60c _ZNKSt3__18functionIFvvEEclEv + 24",
"14 RNTester 0x00000001034dbd24 ___ZN12_GLOBAL__N_128MethodQueueNativeCallInvoker11invokeAsyncEONSt3__18functionIFvvEEE_block_invoke + 44",
"15 libdispatch.dylib 0x0000000106820528 _dispatch_call_block_and_release + 24",
"16 libdispatch.dylib 0x0000000106821d50 _dispatch_client_callout + 16",
"17 libdispatch.dylib 0x0000000106832808 _dispatch_main_queue_drain + 1316",
"18 libdispatch.dylib 0x00000001068322d4 _dispatch_main_queue_callback_4CF + 40",
"19 CoreFoundation 0x000000018039a784 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12",
"20 CoreFoundation 0x0000000180394de4 __CFRunLoopRun + 1912",
"21 CoreFoundation 0x0000000180394254 CFRunLoopRunSpecific + 584",
"22 GraphicsServices 0x0000000188eb7c9c GSEventRunModal + 160",
"23 UIKitCore 0x0000000112b32ff0 -[UIApplication _run] + 868",
"24 UIKitCore 0x0000000112b36f3c UIApplicationMain + 124",
"25 RNTester 0x0000000102e7029c main + 96",
"26 dyld 0x0000000106721514 start_sim + 20",
"27 ??? 0x00000001068fde50 0x0 + 4405059152",
"28 ??? 0x7539800000000000 0x0 + 8446923313598431232"
],
"message": "intentional promise rejection",
"domain": "RCTSampleTurboModule",
"userInfo": {}
}
}
How I logged out the Errors:
console.log('Error in JS:', JSON.stringify({
name: e.name,
message: e.message,
stack: e.stack,
...e,
}, null, 2));
In other places in the codebase we've wrapped this as
try { @try { ... } @catch { // handle obj-c exceptions } } catch { // handle c++ }
https://github.com/facebook/react-native/pull/37484#discussion_r1348806181 @javache I can use that since it's battle-tested. I don't know if using only try has any side effects. I tested all the exceptions from the catch block and all were watched correctly.
| Platform | Engine | Arch | Size (bytes) | Diff |
|---|---|---|---|---|
| android | hermes | arm64-v8a | 21,367,652 | +171 |
| android | hermes | armeabi-v7a | n/a | -- |
| android | hermes | x86 | n/a | -- |
| android | hermes | x86_64 | n/a | -- |
| android | jsc | arm64-v8a | 24,564,228 | +7 |
| android | jsc | armeabi-v7a | n/a | -- |
| android | jsc | x86 | n/a | -- |
| android | jsc | x86_64 | n/a | -- |
Base commit: 2eb7bcb8d9c0f239a13897e3a5d4397d81d3f627 Branch: main
@javache Hi, is there anything blocking this PR?
@javache Thank you 🙏, no worries, will update the PR.
Hi @javache, can you check this PR?
@javache Thank you very much, I've applied the suggestions.
This PR is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.
This PR was closed because it has been stalled for 7 days with no activity.