mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
7612e6601a
Summary: ### [iOS change here](https://github.com/facebook/react-native/pull/40764) 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: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [GENERAL][ADDED] - Turbo Modules Promise-returning functions reject with JS and platform stack traces information Pull Request resolved: https://github.com/facebook/react-native/pull/37484 Test Plan: | Android | |--------| |  | Example of intentionally rejected promise on Android: ``` { "name": "Error", "message": "Exception in HostFunction: intentional promise rejection", "stack": "[native code]\ntryCallTwo@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:25844:9\ndoResolve@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:25975:25\nPromise@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:25863:14\n[native code]\nrejectPromise@http://10.0.2.2:8081/js/examples/TurboModule/SampleTurboModuleExample.bundle?platform=android&lazy=true&app=com.facebook.react.uiapp&modulesOnly=true&dev=true&minify=false&runModule=true&shallow=true:42:70\nonPress@http://10.0.2.2:8081/js/examples/TurboModule/SampleTurboModuleExample.bundle?platform=android&lazy=true&app=com.facebook.react.uiapp&modulesOnly=true&dev=true&minify=false&runModule=true&shallow=true:242:71\n_performTransitionSideEffects@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:51896:22\n_receiveSignal@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:51852:45\nonResponderRelease@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:51715:34\ninvokeGuardedCallbackProd@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:2962:21\ninvokeGuardedCallback@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:3048:42\ninvokeGuardedCallbackAndCatchFirstError@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:3051:36\nexecuteDispatch@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:3115:48\nexecuteDispatchesInOrder@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:3132:26\nexecuteDispatchesAndRelease@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:4856:35\nforEach@[native code]\nforEachAccumulated@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:3574:22\nrunEventsInBatch@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:4874:27\nrunExtractedPluginEventsInBatch@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:4896:25\nhttp://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:4914:42\nbatchedUpdates$1@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:14750:20\nbatchedUpdates@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:4845:36\ndispatchEvent@http://10.0.2.2:8081/js/RNTesterApp.android.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.facebook.react.uiapp&modulesOnly=false&runModule=true:4907:23", "cause": { "nativeStackAndroid": [ { "lineNumber": 173, "file": "SampleTurboModule.java", "methodName": "getValueWithPromise", "class": "com.facebook.fbreact.specs.SampleTurboModule" }, { "lineNumber": -2, "file": "NativeRunnable.java", "methodName": "run", "class": "com.facebook.jni.NativeRunnable" }, { "lineNumber": 942, "file": "Handler.java", "methodName": "handleCallback", "class": "android.os.Handler" }, { "lineNumber": 99, "file": "Handler.java", "methodName": "dispatchMessage", "class": "android.os.Handler" }, { "lineNumber": 27, "file": "MessageQueueThreadHandler.java", "methodName": "dispatchMessage", "class": "com.facebook.react.bridge.queue.MessageQueueThreadHandler" }, { "lineNumber": 201, "file": "Looper.java", "methodName": "loopOnce", "class": "android.os.Looper" }, { "lineNumber": 288, "file": "Looper.java", "methodName": "loop", "class": "android.os.Looper" }, { "lineNumber": 228, "file": "MessageQueueThreadImpl.java", "methodName": "run", "class": "com.facebook.react.bridge.queue.MessageQueueThreadImpl$4" }, { "lineNumber": 1012, "file": "Thread.java", "methodName": "run", "class": "java.lang.Thread" } ], "userInfo": null, "message": "intentional promise rejection", "code": "code 1" } } ``` How I logged out the Errors: ```js console.log('Error in JS:', JSON.stringify({ name: e.name, message: e.message, stack: e.stack, ...e, }, null, 2)); ``` Reviewed By: RSNara Differential Revision: D50613349 Pulled By: javache fbshipit-source-id: b49c469118c8d8d27c43164f110dfe57ddd592d9
238 lines
6.7 KiB
JavaScript
238 lines
6.7 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
* @flow strict-local
|
|
*/
|
|
|
|
import NativeSampleTurboModule from 'react-native/Libraries/TurboModule/samples/NativeSampleTurboModule';
|
|
import {EnumInt} from 'react-native/Libraries/TurboModule/samples/NativeSampleTurboModule';
|
|
import type {RootTag} from 'react-native/Libraries/ReactNative/RootTag';
|
|
import {
|
|
Text,
|
|
View,
|
|
FlatList,
|
|
TouchableOpacity,
|
|
RootTagContext,
|
|
} from 'react-native';
|
|
|
|
import styles from './TurboModuleExampleCommon';
|
|
import * as React from 'react';
|
|
|
|
type State = {|
|
|
testResults: {
|
|
[string]: {
|
|
type: string,
|
|
value: mixed,
|
|
...
|
|
},
|
|
...
|
|
},
|
|
|};
|
|
|
|
class SampleTurboModuleExample extends React.Component<{||}, State> {
|
|
static contextType: React$Context<RootTag> = RootTagContext;
|
|
|
|
state: State = {
|
|
testResults: {},
|
|
};
|
|
|
|
// Add calls to methods in TurboModule here
|
|
// $FlowFixMe[missing-local-annot]
|
|
_tests = {
|
|
callback: () =>
|
|
NativeSampleTurboModule.getValueWithCallback(callbackValue =>
|
|
this._setResult('callback', callbackValue),
|
|
),
|
|
promise: () =>
|
|
NativeSampleTurboModule.getValueWithPromise(false).then(valuePromise =>
|
|
this._setResult('promise', valuePromise),
|
|
),
|
|
rejectPromise: () =>
|
|
NativeSampleTurboModule.getValueWithPromise(true)
|
|
.then(() => {})
|
|
.catch(e => {
|
|
console.error(e);
|
|
this._setResult('rejectPromise', e.message);
|
|
}),
|
|
getConstants: () => NativeSampleTurboModule.getConstants(),
|
|
voidFunc: () => NativeSampleTurboModule.voidFunc(),
|
|
getBool: () => NativeSampleTurboModule.getBool(true),
|
|
getEnum: () =>
|
|
NativeSampleTurboModule.getEnum
|
|
? NativeSampleTurboModule.getEnum(EnumInt.A)
|
|
: null,
|
|
getNumber: () => NativeSampleTurboModule.getNumber(99.95),
|
|
getString: () => NativeSampleTurboModule.getString('Hello'),
|
|
getArray: () =>
|
|
NativeSampleTurboModule.getArray([
|
|
{a: 1, b: 'foo'},
|
|
{a: 2, b: 'bar'},
|
|
null,
|
|
]),
|
|
getObject: () =>
|
|
NativeSampleTurboModule.getObject({a: 1, b: 'foo', c: null}),
|
|
getUnsafeObject: () =>
|
|
NativeSampleTurboModule.getObject({a: 1, b: 'foo', c: null}),
|
|
getRootTag: () => NativeSampleTurboModule.getRootTag(this.context),
|
|
getValue: () =>
|
|
NativeSampleTurboModule.getValue(5, 'test', {a: 1, b: 'foo'}),
|
|
voidFuncThrows: () => {
|
|
try {
|
|
NativeSampleTurboModule.voidFuncThrows?.();
|
|
} catch (e) {
|
|
console.error(e);
|
|
return e.message;
|
|
}
|
|
},
|
|
getObjectThrows: () => {
|
|
try {
|
|
NativeSampleTurboModule.getObjectThrows?.({a: 1, b: 'foo', c: null});
|
|
} catch (e) {
|
|
console.error(e);
|
|
return e.message;
|
|
}
|
|
},
|
|
promiseThrows: () => {
|
|
NativeSampleTurboModule.promiseThrows?.()
|
|
.then(() => {})
|
|
.catch(e => {
|
|
console.error(e);
|
|
this._setResult('promiseThrows', e.message);
|
|
});
|
|
},
|
|
voidFuncAssert: () => {
|
|
try {
|
|
NativeSampleTurboModule.voidFuncAssert?.();
|
|
} catch (e) {
|
|
console.error(e);
|
|
return e.message;
|
|
}
|
|
},
|
|
getObjectAssert: () => {
|
|
try {
|
|
NativeSampleTurboModule.getObjectAssert?.({a: 1, b: 'foo', c: null});
|
|
} catch (e) {
|
|
console.error(e);
|
|
return e.message;
|
|
}
|
|
},
|
|
promiseAssert: () => {
|
|
NativeSampleTurboModule.promiseAssert?.()
|
|
.then(() => {})
|
|
.catch(e => {
|
|
console.error(e);
|
|
this._setResult('promiseAssert', e.message);
|
|
});
|
|
},
|
|
};
|
|
|
|
_setResult(
|
|
name:
|
|
| string
|
|
| 'callback'
|
|
| 'getArray'
|
|
| 'getBool'
|
|
| 'getEnum'
|
|
| 'getConstants'
|
|
| 'getNumber'
|
|
| 'getObject'
|
|
| 'getRootTag'
|
|
| 'getString'
|
|
| 'getUnsafeObject'
|
|
| 'getValue'
|
|
| 'promise'
|
|
| 'rejectPromise'
|
|
| 'voidFunc'
|
|
| 'voidFuncThrows'
|
|
| 'getObjectThrows'
|
|
| 'promiseThrows'
|
|
| 'voidFuncAssert'
|
|
| 'getObjectAssert'
|
|
| 'promiseAssert',
|
|
result:
|
|
| $FlowFixMe
|
|
| void
|
|
| RootTag
|
|
| Promise<mixed>
|
|
| number
|
|
| string
|
|
| boolean
|
|
| {const1: boolean, const2: number, const3: string}
|
|
| Array<$FlowFixMe>,
|
|
) {
|
|
this.setState(({testResults}) => ({
|
|
/* $FlowFixMe[cannot-spread-indexer] (>=0.122.0 site=react_native_fb)
|
|
* This comment suppresses an error found when Flow v0.122.0 was
|
|
* deployed. To see the error, delete this comment and run Flow. */
|
|
testResults: {
|
|
...testResults,
|
|
/* $FlowFixMe[invalid-computed-prop] (>=0.111.0 site=react_native_fb)
|
|
* This comment suppresses an error found when Flow v0.111 was
|
|
* deployed. To see the error, delete this comment and run Flow. */
|
|
[name]: {value: result, type: typeof result},
|
|
},
|
|
}));
|
|
}
|
|
|
|
_renderResult(name: string): React.Node {
|
|
const result = this.state.testResults[name] || {};
|
|
return (
|
|
<View style={styles.result}>
|
|
<Text style={[styles.value]}>{JSON.stringify(result.value)}</Text>
|
|
<Text style={[styles.type]}>{result.type}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
componentDidMount(): void {
|
|
if (global.__turboModuleProxy == null) {
|
|
throw new Error(
|
|
'Cannot load this example because TurboModule is not configured.',
|
|
);
|
|
}
|
|
}
|
|
|
|
render(): React.Node {
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.item}>
|
|
<TouchableOpacity
|
|
style={[styles.column, styles.button]}
|
|
onPress={() =>
|
|
Object.keys(this._tests).forEach(item =>
|
|
this._setResult(item, this._tests[item]()),
|
|
)
|
|
}>
|
|
<Text style={styles.buttonTextLarge}>Run all tests</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
onPress={() => this.setState({testResults: {}})}
|
|
style={[styles.column, styles.button]}>
|
|
<Text style={styles.buttonTextLarge}>Clear results</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<FlatList
|
|
data={Object.keys(this._tests)}
|
|
keyExtractor={item => item}
|
|
renderItem={({item}) => (
|
|
<View style={styles.item}>
|
|
<TouchableOpacity
|
|
style={[styles.column, styles.button]}
|
|
onPress={e => this._setResult(item, this._tests[item]())}>
|
|
<Text style={styles.buttonText}>{item}</Text>
|
|
</TouchableOpacity>
|
|
<View style={[styles.column]}>{this._renderResult(item)}</View>
|
|
</View>
|
|
)}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = SampleTurboModuleExample;
|