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 035d071bf19b00..11dbbab63a5838 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,17 @@ 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 724d91e3ccb3d1..c5007db5943dd7 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,24 @@ 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_; @@ -101,6 +119,8 @@ struct JPromiseImpl : public jni::JavaClass { } }; +jsi::Value createJSRuntimeError(jsi::Runtime &runtime, const std::string &message); + // This is used for generating short exception strings. std::string stringifyJSIValue(const jsi::Value& v, jsi::Runtime* rt = nullptr) { if (v.isUndefined()) { @@ -395,33 +415,54 @@ jsi::Value createJSRuntimeError( * Creates JSError with current JS runtime stack and Throwable stack trace. */ jsi::JSError convertThrowableToJSError( - jsi::Runtime& runtime, - jni::local_ref throwable) { - auto stackTrace = throwable->getStackTrace(); - - jsi::Array stackElements(runtime, stackTrace->size()); - for (int i = 0; i < stackTrace->size(); ++i) { - auto frame = stackTrace->getElement(i); - - jsi::Object frameObject(runtime); - frameObject.setProperty(runtime, "className", frame->getClassName()); - frameObject.setProperty(runtime, "fileName", frame->getFileName()); - frameObject.setProperty(runtime, "lineNumber", frame->getLineNumber()); - frameObject.setProperty(runtime, "methodName", frame->getMethodName()); - stackElements.setValueAtIndex(runtime, i, std::move(frameObject)); - } + jsi::Runtime& runtime, + jni::local_ref throwable) { + auto stackTrace = throwable->getStackTrace(); + + jsi::Array stackElements(runtime, stackTrace->size()); + for (int i = 0; i < stackTrace->size(); ++i) { + auto frame = stackTrace->getElement(i); + + jsi::Object frameObject(runtime); + frameObject.setProperty(runtime, "className", frame->getClassName()); + frameObject.setProperty(runtime, "fileName", frame->getFileName()); + frameObject.setProperty(runtime, "lineNumber", frame->getLineNumber()); + frameObject.setProperty(runtime, "methodName", frame->getMethodName()); + stackElements.setValueAtIndex(runtime, i, std::move(frameObject)); + } - jsi::Object cause(runtime); - auto name = throwable->getClass()->getCanonicalName()->toStdString(); - auto message = throwable->getMessage()->toStdString(); - cause.setProperty(runtime, "name", name); - cause.setProperty(runtime, "message", message); - cause.setProperty(runtime, "stackElements", std::move(stackElements)); - - jsi::Value error = - createJSRuntimeError(runtime, "Exception in HostFunction: " + message); - error.asObject(runtime).setProperty(runtime, "cause", std::move(cause)); - return {runtime, std::move(error)}; + jsi::Object cause(runtime); + auto name = throwable->getClass()->getCanonicalName()->toStdString(); + auto message = throwable->getMessage()->toStdString(); + cause.setProperty(runtime, "name", name); + cause.setProperty(runtime, "message", message); + cause.setProperty(runtime, "stackElements", std::move(stackElements)); + + jsi::Value error = + createJSRuntimeError(runtime, "Exception in HostFunction: " + message); + error.asObject(runtime).setProperty(runtime, "cause", std::move(cause)); + return {runtime, std::move(error)}; +} + +void rejectWithException( + AsyncCallback<> &reject, + std::exception_ptr exception, + std::optional &jsInvocationStack) { + auto localThrowable = jni::getJavaExceptionForCppException(exception); + jni::global_ref globalThrowable = jni::make_global(localThrowable.get()); + + reject.call([ + jsInvocationStack, + globalThrowable + ](jsi::Runtime& rt, jsi::Function& jsFunction) { + auto jsError = convertThrowableToJSError(rt,jni::make_local(globalThrowable)); + + if (jsInvocationStack.has_value()) { + jsError.value().asObject(rt).setProperty(rt, "stack", jsInvocationStack.value()); + } + + jsFunction.call(rt, jsError.value()); + }); } } // namespace @@ -774,6 +815,9 @@ 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( @@ -790,6 +834,10 @@ 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), @@ -808,14 +856,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_), @@ -838,7 +897,13 @@ 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 57a2be7f582425..5331195cf0a1f3 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 => { + this._setResult('rejectPromise', e.message); + console.error(e, e.stack, e.cause); + }), 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, e.stack, e.cause); 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, e.stack, e.cause); return e.message; } }, promiseThrows: () => { - try { - // $FlowFixMe[unused-promise] - NativeSampleTurboModule.promiseThrows?.(); - } catch (e) { - return e.message; - } + NativeSampleTurboModule.promiseThrows?.() + .then(() => {}) + .catch(e => { + this._setResult('promiseThrows', e.message); + console.error(e, e.stack, e.cause); + }); }, voidFuncAssert: () => { try { NativeSampleTurboModule.voidFuncAssert?.(); } catch (e) { + console.error(e, e.stack, e.cause); 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, e.stack, e.cause); return e.message; } }, promiseAssert: () => { - try { - // $FlowFixMe[unused-promise] - NativeSampleTurboModule.promiseAssert?.(); - } catch (e) { - return e.message; - } + NativeSampleTurboModule.promiseAssert?.() + .then(() => {}) + .catch(e => { + this._setResult('promiseAssert', e.message); + console.error(e, e.stack, e.cause); + }); }, };