diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 035d071bf19..366da86b585 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -8,6 +8,7 @@ package com.facebook.react.config; import com.facebook.proguard.annotations.DoNotStripAny; +import com.facebook.react.common.build.ReactBuildConfig; /** * Hi there, traveller! This configuration class is not meant to be used by end-users of RN. It @@ -161,4 +162,16 @@ public class ReactFeatureFlags { * priorities from any thread. */ public static boolean useModernRuntimeScheduler = false; + + /** + * Enables storing js caller stack when creating promise in native module. This is useful in case + * of Promise rejection and tracing the cause. + */ + public static boolean traceTurboModulePromiseRejections = ReactBuildConfig.DEBUG; + + /** + * Enables auto rejecting promises from Turbo Modules method calls. If native error occurs Promise + * in JS will be rejected (The JS error will include native stack) + */ + public static boolean rejectTurboModulePromiseOnNativeError = true; } diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp index 2153e5e8e7c..a28b38fa1de 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp @@ -58,6 +58,28 @@ JavaTurboModule::~JavaTurboModule() { namespace { +constexpr auto kReactFeatureFlagsJavaDescriptor = + "com/facebook/react/config/ReactFeatureFlags"; + +bool getFeatureFlagBoolValue(const char* name) { + static const auto reactFeatureFlagsClass = + facebook::jni::findClassStatic(kReactFeatureFlagsJavaDescriptor); + const auto field = reactFeatureFlagsClass->getStaticField(name); + return reactFeatureFlagsClass->getStaticFieldValue(field); +} + +bool traceTurboModulePromiseRejections() { + static bool traceRejections = + getFeatureFlagBoolValue("traceTurboModulePromiseRejections"); + return traceRejections; +} + +bool rejectTurboModulePromiseOnNativeError() { + static bool rejectOnError = + getFeatureFlagBoolValue("rejectTurboModulePromiseOnNativeError"); + return rejectOnError; +} + struct JNIArgs { JNIArgs(size_t count) : args_(count) {} std::vector args_; @@ -396,7 +418,7 @@ jsi::Value createJSRuntimeError( */ jsi::JSError convertThrowableToJSError( jsi::Runtime& runtime, - jni::local_ref throwable) { + jni::alias_ref throwable) { auto stackTrace = throwable->getStackTrace(); jsi::Array stackElements(runtime, stackTrace->size()); @@ -424,6 +446,22 @@ jsi::JSError convertThrowableToJSError( return {runtime, std::move(error)}; } +void rejectWithException( + AsyncCallback<>& reject, + std::exception_ptr exception, + std::optional& jsInvocationStack) { + auto throwable = jni::getJavaExceptionForCppException(exception); + reject.call([jsInvocationStack, throwable = jni::make_global(throwable)]( + jsi::Runtime& rt, jsi::Function& jsFunction) { + auto jsError = convertThrowableToJSError(rt, throwable); + if (jsInvocationStack.has_value()) { + jsError.value().asObject(rt).setProperty( + rt, "stack", jsInvocationStack.value()); + } + jsFunction.call(rt, jsError.value()); + }); +} + } // namespace jsi::Value JavaTurboModule::invokeJavaMethod( @@ -783,6 +821,10 @@ jsi::Value JavaTurboModule::invokeJavaMethod( jsi::Function Promise = runtime.global().getPropertyAsFunction(runtime, "Promise"); + // The callback is used for auto rejecting if error is caught from method + // invocation + std::optional> nativeRejectCallback; + // The promise constructor runs its arg immediately, so this is safe jobject javaPromise; jsi::Value jsPromise = Promise.callAsConstructor( @@ -799,6 +841,13 @@ jsi::Value JavaTurboModule::invokeJavaMethod( throw jsi::JSError(runtime, "Incorrect number of arguments"); } + if (rejectTurboModulePromiseOnNativeError()) { + nativeRejectCallback = AsyncCallback( + runtime, + args[1].getObject(runtime).getFunction(runtime), + jsInvoker_); + } + auto resolve = createJavaCallback( runtime, args[0].getObject(runtime).getFunction(runtime), @@ -817,14 +866,25 @@ jsi::Value JavaTurboModule::invokeJavaMethod( env->DeleteLocalRef(javaPromise); jargs[argCount].l = globalPromise; + // JS Stack at the time when the promise is created. + std::optional jsInvocationStack; + if (traceTurboModulePromiseRejections()) { + jsInvocationStack = createJSRuntimeError(runtime, "") + .asObject(runtime) + .getProperty(runtime, "stack") + .toString(runtime) + .utf8(runtime); + } + const char* moduleName = name_.c_str(); const char* methodName = methodNameStr.c_str(); TMPL::asyncMethodCallArgConversionEnd(moduleName, methodName); - TMPL::asyncMethodCallDispatch(moduleName, methodName); nativeMethodCallInvoker_->invokeAsync( methodName, [jargs, + rejectCallback = std::move(nativeRejectCallback), + jsInvocationStack = std::move(jsInvocationStack), globalRefs, methodID, instance_ = jni::make_weak(instance_), @@ -856,7 +916,14 @@ jsi::Value JavaTurboModule::invokeJavaMethod( FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); } catch (...) { TMPL::asyncMethodCallExecutionFail(moduleName, methodName, id); - throw; + if (rejectTurboModulePromiseOnNativeError() && rejectCallback) { + auto exception = std::current_exception(); + rejectWithException( + *rejectCallback, exception, jsInvocationStack); + rejectCallback = std::nullopt; + } else { + throw; + } } for (auto globalRef : globalRefs) { diff --git a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js index 57a2be7f582..283c557bb2a 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -54,7 +54,10 @@ class SampleTurboModuleExample extends React.Component<{||}, State> { rejectPromise: () => NativeSampleTurboModule.getValueWithPromise(true) .then(() => {}) - .catch(e => this._setResult('rejectPromise', e.message)), + .catch(e => { + console.error(e); + this._setResult('rejectPromise', e.message); + }), getConstants: () => NativeSampleTurboModule.getConstants(), voidFunc: () => NativeSampleTurboModule.voidFunc(), getBool: () => NativeSampleTurboModule.getBool(true), @@ -81,6 +84,7 @@ class SampleTurboModuleExample extends React.Component<{||}, State> { try { NativeSampleTurboModule.voidFuncThrows?.(); } catch (e) { + console.error(e); return e.message; } }, @@ -88,21 +92,23 @@ class SampleTurboModuleExample extends React.Component<{||}, State> { try { NativeSampleTurboModule.getObjectThrows?.({a: 1, b: 'foo', c: null}); } catch (e) { + console.error(e); return e.message; } }, promiseThrows: () => { - try { - // $FlowFixMe[unused-promise] - NativeSampleTurboModule.promiseThrows?.(); - } catch (e) { - return e.message; - } + 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; } }, @@ -110,16 +116,17 @@ class SampleTurboModuleExample extends React.Component<{||}, State> { try { NativeSampleTurboModule.getObjectAssert?.({a: 1, b: 'foo', c: null}); } catch (e) { + console.error(e); return e.message; } }, promiseAssert: () => { - try { - // $FlowFixMe[unused-promise] - NativeSampleTurboModule.promiseAssert?.(); - } catch (e) { - return e.message; - } + NativeSampleTurboModule.promiseAssert?.() + .then(() => {}) + .catch(e => { + console.error(e); + this._setResult('promiseAssert', e.message); + }); }, };