Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tm-android): Reject Promise if Turbo Module method throws an Error #37484

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
097525d
Add iOS reject promise if turbo module method throws exception
krystofwoldrich Apr 27, 2023
4fc71fd
Merge remote-tracking branch 'origin/main' into kw-mixed-stack-traces…
krystofwoldrich May 3, 2023
108e205
Merge remote-tracking branch 'origin/main' into kw-mixed-stack-traces…
krystofwoldrich May 14, 2023
0ca58bb
Merge branch 'main' into kw-mixed-stack-traces-promise
krystofwoldrich May 15, 2023
5892e50
Add Android reject async func on error
krystofwoldrich May 15, 2023
97606c9
add(ios): js stack to rejection caused by native exception or native …
krystofwoldrich May 16, 2023
2eb0a46
Fix android promise reject and add JS stack trace
krystofwoldrich May 17, 2023
a836691
Refactor java callback
krystofwoldrich May 18, 2023
be2ea63
Fix typos
krystofwoldrich May 18, 2023
ccf5460
iOS jsStack do not copy
krystofwoldrich May 18, 2023
85f48d7
Update lint
krystofwoldrich May 18, 2023
7205f1d
More lint
krystofwoldrich May 18, 2023
d92339c
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
krystofwoldrich Jun 6, 2023
99cb0b9
Add RCT_TRACE_PROMISE_REJECTION_ENABLED flag for iOS
krystofwoldrich Jun 6, 2023
732dba9
Rename the iOS flag
krystofwoldrich Jun 6, 2023
239667d
Add android flag and fix internal reject invocation on Hermes
krystofwoldrich Jun 14, 2023
882c81b
move RCTInternalPromiseRejectBlock to rct turbo module
krystofwoldrich Jun 14, 2023
9529dd9
Add js func returning void rethrow explanation
krystofwoldrich Jun 14, 2023
40163ff
Rename RCTInternalPromiseRejectBlock to RCTNSExceptionHandler
krystofwoldrich Jun 15, 2023
49b861d
Add CallbackWrapperExecutor type
krystofwoldrich Jun 15, 2023
4d40ceb
Handle all throws from Obj-Cpp
krystofwoldrich Jun 15, 2023
86bf079
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
krystofwoldrich Aug 9, 2023
b21990c
Merge branch 'main' into kw-mixed-stack-traces-promise
krystofwoldrich Aug 10, 2023
25b32b7
Enable trace turbo module rejection in debug iOS builds
krystofwoldrich Aug 11, 2023
be8e027
Make jsInvocationStack optional
krystofwoldrich Aug 11, 2023
377c328
Add jsInvocationStack comment
krystofwoldrich Aug 11, 2023
6e4b652
use ref to js stack in createRejectJSErrorValue
krystofwoldrich Aug 11, 2023
abf7e8a
Use only one rejectJSIFn in java
krystofwoldrich Aug 11, 2023
e1634a6
Rename feature flag
krystofwoldrich Aug 11, 2023
65982e2
fix flag func overload
krystofwoldrich Aug 11, 2023
ba06a6e
Update feature flag
krystofwoldrich Aug 11, 2023
c6dd575
Merge commit '60f5a80c1d5900e5213aabd3d3a762174996cfd1' into kw-mixed…
krystofwoldrich Oct 9, 2023
2a3f8ea
enable tracing turbo modules promises rejections in debug
krystofwoldrich Oct 9, 2023
bd9b7f1
unify naming
krystofwoldrich Oct 9, 2023
3f81d5a
Use jsi types for callback executor
krystofwoldrich Oct 10, 2023
1936174
access cause safe
krystofwoldrich Oct 10, 2023
975a511
keep js invocation stack as property
krystofwoldrich Oct 10, 2023
bf584da
Hold reference to stack to avoid parsing string
krystofwoldrich Oct 10, 2023
3937f61
Add explanation comment
krystofwoldrich Oct 10, 2023
c735ba9
Add test errors logs for easier verification
krystofwoldrich Oct 10, 2023
7e84c08
Add rejectTurboModulePromiseOnNativeError flag
krystofwoldrich Oct 10, 2023
f0f515b
Drop iOS changes
krystofwoldrich Oct 10, 2023
7359585
One more header
krystofwoldrich Oct 10, 2023
0025fbd
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
krystofwoldrich Oct 18, 2023
422f10f
review fixes, remove build config, unwrap callback executor, safe han…
krystofwoldrich Oct 19, 2023
b0a1ed5
Remove unused var
krystofwoldrich Oct 19, 2023
26683c5
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
krystofwoldrich Oct 23, 2023
fdbde12
Use async callback WIP -> returns undefined
krystofwoldrich Oct 24, 2023
d545623
Clean up, fix reject index, revert stack to string add comments
krystofwoldrich Oct 24, 2023
d77f909
trace rejection based on debug config flag
krystofwoldrich Oct 24, 2023
bc505bf
Update based on comments
krystofwoldrich Oct 24, 2023
e99fb6a
Use global ref for throwable
krystofwoldrich Oct 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<jboolean>(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<jvalue> args_;
Expand Down Expand Up @@ -101,6 +119,8 @@ struct JPromiseImpl : public jni::JavaClass<JPromiseImpl> {
}
};

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()) {
Expand Down Expand Up @@ -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<jni::JThrowable> 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<jni::JThrowable> 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<std::string> &jsInvocationStack) {
auto localThrowable = jni::getJavaExceptionForCppException(exception);
jni::global_ref<jni::JThrowable> 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
Expand Down Expand Up @@ -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<AsyncCallback<>> nativeRejectCallback;

// The promise constructor runs its arg immediately, so this is safe
jobject javaPromise;
jsi::Value jsPromise = Promise.callAsConstructor(
Expand All @@ -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),
Expand All @@ -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<std::string> 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_),
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -81,45 +84,49 @@ class SampleTurboModuleExample extends React.Component<{||}, State> {
try {
NativeSampleTurboModule.voidFuncThrows?.();
} catch (e) {
console.error(e, e.stack, e.cause);
return e.message;
}
},
getObjectThrows: () => {
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;
}
},
getObjectAssert: () => {
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);
});
},
};

Expand Down