Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 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…
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
May 15, 2023
5892e50
Add Android reject async func on error
May 15, 2023
97606c9
add(ios): js stack to rejection caused by native exception or native …
May 16, 2023
2eb0a46
Fix android promise reject and add JS stack trace
May 17, 2023
a836691
Refactor java callback
May 18, 2023
be2ea63
Fix typos
May 18, 2023
ccf5460
iOS jsStack do not copy
May 18, 2023
85f48d7
Update lint
May 18, 2023
7205f1d
More lint
May 18, 2023
d92339c
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
Jun 6, 2023
99cb0b9
Add RCT_TRACE_PROMISE_REJECTION_ENABLED flag for iOS
Jun 6, 2023
732dba9
Rename the iOS flag
Jun 6, 2023
239667d
Add android flag and fix internal reject invocation on Hermes
Jun 14, 2023
882c81b
move RCTInternalPromiseRejectBlock to rct turbo module
Jun 14, 2023
9529dd9
Add js func returning void rethrow explanation
Jun 14, 2023
40163ff
Rename RCTInternalPromiseRejectBlock to RCTNSExceptionHandler
Jun 15, 2023
49b861d
Add CallbackWrapperExecutor type
Jun 15, 2023
4d40ceb
Handle all throws from Obj-Cpp
Jun 15, 2023
86bf079
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
Aug 9, 2023
b21990c
Merge branch 'main' into kw-mixed-stack-traces-promise
Aug 10, 2023
25b32b7
Enable trace turbo module rejection in debug iOS builds
Aug 11, 2023
be8e027
Make jsInvocationStack optional
Aug 11, 2023
377c328
Add jsInvocationStack comment
Aug 11, 2023
6e4b652
use ref to js stack in createRejectJSErrorValue
Aug 11, 2023
abf7e8a
Use only one rejectJSIFn in java
Aug 11, 2023
e1634a6
Rename feature flag
Aug 11, 2023
65982e2
fix flag func overload
Aug 11, 2023
ba06a6e
Update feature flag
Aug 11, 2023
c6dd575
Merge commit '60f5a80c1d5900e5213aabd3d3a762174996cfd1' into kw-mixed…
Oct 9, 2023
b01e866
clean up rct bridge
Oct 9, 2023
2a3f8ea
enable tracing turbo modules promises rejections in debug
Oct 9, 2023
bd9b7f1
unify naming
Oct 9, 2023
3f81d5a
Use jsi types for callback executor
Oct 10, 2023
1936174
access cause safe
Oct 10, 2023
975a511
keep js invocation stack as property
Oct 10, 2023
bf584da
Hold reference to stack to avoid parsing string
Oct 10, 2023
3937f61
Add explanation comment
Oct 10, 2023
c735ba9
Add test errors logs for easier verification
Oct 10, 2023
7e84c08
Add rejectTurboModulePromiseOnNativeError flag
Oct 10, 2023
2625edb
Merge commit '7e84c08ef6cc7624f922f2558dd40bec1aa5da8f' into kw-mixed…
Oct 10, 2023
c9c099b
Drop java changes
Oct 10, 2023
b5471d2
fix perform invocation
Oct 10, 2023
b3cfec3
use double try to catch errors
Oct 10, 2023
f19abd3
Add example cpp error to the ios turbo modules
Oct 10, 2023
95480e1
Remove unwanted Android changes
Oct 11, 2023
707f5aa
Once more remove Android changes
Oct 11, 2023
a48a1f2
Gate promise rejection on native error
Oct 11, 2023
d082716
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
Oct 18, 2023
2fdd778
rename promise reject block to RCTInvocationErrorHandler
Oct 23, 2023
11e9b9b
Use explicit JSError constructor
Oct 23, 2023
b1ea76e
clear handleCause return statements
Oct 23, 2023
af39197
refactor try catch use maybecatch
Oct 23, 2023
464f6bc
remove unnecessary copy
Oct 23, 2023
efa52b6
remove extra internal reject wrapper
Oct 23, 2023
d0dccce
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
Oct 25, 2023
ae80dc7
Cleanup
Oct 25, 2023
d5aa06b
Fix lint
Oct 25, 2023
5eb29ae
Merge branch 'main' into kw-mixed-stack-traces-promise-ios
krystofwoldrich Mar 10, 2024
be13239
Fix merge
krystofwoldrich Mar 10, 2024
f2e6d32
Fix review comments
krystofwoldrich Mar 10, 2024
229ec73
remove duplicate imports
krystofwoldrich Mar 10, 2024
41fac96
Merge remote-tracking branch 'origin/main' into kw-mixed-stack-traces…
Jul 14, 2024
bfb410c
fixing review comments
Jul 14, 2024
04fb46e
fix createJSRuntimeError and NSException parsing
Jul 15, 2024
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
10 changes: 10 additions & 0 deletions packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
{
RCTEnableTurboModule(turboModuleEnabled);

#ifndef RCT_TRACE_PROMISE_REJECTION_ENABLED
#if DEBUG
#define RCT_TRACE_PROMISE_REJECTION_ENABLED 1
#endif
#endif

#if RCT_TRACE_PROMISE_REJECTION_ENABLED
RCTEnableTraceTurboModulePromiseRejections(YES);
#endif

#if DEBUG
// Disable idle timer in dev builds to avoid putting application in background and complicating
// Metro reconnection logic. Users only need this when running the application using our CLI tooling.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ else
source[:tag] = "v#{version}"
end

trace_promise_rejections_enabled = ENV["RCT_TRACE_PROMISE_REJECTION_ENABLED"] == "1"
trace_promise_rejections_flag = (trace_promise_rejections_enabled ? " -DRCT_TRACE_PROMISE_REJECTION_ENABLED" : "")

folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
Expand All @@ -26,7 +29,7 @@ use_hermes = ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == '1'
new_arch_enabled_flag = (is_new_arch_enabled ? " -DRCT_NEW_ARCH_ENABLED" : "")
is_fabric_enabled = true #is_new_arch_enabled || ENV["RCT_FABRIC_ENABLED"]
hermes_flag = (use_hermes ? " -DUSE_HERMES" : "")
other_cflags = "$(inherited)" + folly_compiler_flags + new_arch_enabled_flag + hermes_flag
other_cflags = "$(inherited)" + folly_compiler_flags + new_arch_enabled_flag + hermes_flag + trace_promise_rejections_flag

header_search_paths = [
"$(PODS_TARGET_SRCROOT)/../../ReactCommon",
Expand Down
8 changes: 8 additions & 0 deletions packages/react-native/React/Base/RCTBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ void RCTSetTurboModuleInteropBridgeProxyLogLevel(RCTBridgeProxyLoggingLevel logL
BOOL RCTTurboModuleInteropForAllTurboModulesEnabled(void);
void RCTEnableTurboModuleInteropForAllTurboModules(BOOL enabled);

// Trace Rejected Promises of Turbo Modules (store callers' js stack)
RCT_EXTERN BOOL RCTTraceTurboModulePromiseRejections(void);
RCT_EXTERN void RCTEnableTraceTurboModulePromiseRejections(BOOL enabled);

// Reject JS Promise on native throw in TM method call
RCT_EXTERN BOOL RCTRejectTurboModulePromiseOnNativeError(void);
RCT_EXTERN void RCTEnableRejectTurboModulePromiseOnNativeError(BOOL enabled);

typedef enum {
kRCTGlobalScope,
kRCTGlobalScopeUsingRetainJSCallback,
Expand Down
22 changes: 22 additions & 0 deletions packages/react-native/React/Base/RCTBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ void RCTEnableTurboModule(BOOL enabled)
turboModuleEnabled = enabled;
}

static BOOL traceTurboModulePromiseRejections = NO;
BOOL RCTTraceTurboModulePromiseRejections(void)
{
return traceTurboModulePromiseRejections;
}

void RCTEnableTraceTurboModulePromiseRejections(BOOL enabled)
{
traceTurboModulePromiseRejections = enabled;
}

static BOOL rejectTurboModulePromiseOnNativeError = YES;
BOOL RCTRejectTurboModulePromiseOnNativeError(void)
{
return rejectTurboModulePromiseOnNativeError;
}

void RCTEnableRejectTurboModulePromiseOnNativeError(BOOL enabled)
{
rejectTurboModulePromiseOnNativeError = enabled;
}

static BOOL turboModuleInteropEnabled = NO;
BOOL RCTTurboModuleInteropEnabled(void)
{
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Base/RCTUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector)
NSArray<NSString *> *stackTrace = [NSThread callStackSymbols];
NSMutableDictionary *userInfo;
NSMutableDictionary<NSString *, id> *errorInfo = [NSMutableDictionary dictionaryWithObject:stackTrace
forKey:@"nativeStackIOS"];
forKey:@"stackSymbols"];

if (error) {
errorMessage = error.localizedDescription ?: @"Unknown error from a native module";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule {
bool isSync,
const char *methodName,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation);
NSMutableArray *retainedObjectsForInvocation,
_Nullable RCTPromiseRejectBlock reject);
void performVoidMethodInvocation(
jsi::Runtime &runtime,
const char *methodName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,14 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s
throw std::runtime_error("Unsupported jsi::Value kind");
}

static jsi::Value createJSRuntimeError(jsi::Runtime &runtime, const std::string &message)
{
return runtime.global().getPropertyAsFunction(runtime, "Error").call(runtime, message);
jsi::Value createJSRuntimeError(jsi::Runtime& runtime, jsi::Value&& message) {
return runtime.global()
.getPropertyAsFunction(runtime, "Error")
.call(runtime, std::move(message));
}

jsi::Value createJSRuntimeError(jsi::Runtime& runtime, const std::string& message) {
return createJSRuntimeError(runtime, jsi::String::createFromUtf8(runtime, message));
}

/**
Expand All @@ -208,17 +213,33 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s

jsi::Value error = createJSRuntimeError(runtime, "Exception in HostFunction: " + reason);
error.asObject(runtime).setProperty(runtime, "cause", std::move(cause));
return {runtime, std::move(error)};
return jsi::JSError(runtime, std::move(error));
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method now unused? Can it be removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/**
* Creates JSError with current JS runtime and NSDictionary data as cause.
*/
static jsi::JSError convertNSDictionaryToJSError(jsi::Runtime &runtime, NSDictionary *cause)
{
jsi::Value error = createJSRuntimeError(runtime, std::string("Exception in HostFunction: ") + (cause[@"message"] ? [cause[@"message"] UTF8String] : "<unknown>"));
error.asObject(runtime).setProperty(runtime, "cause", convertNSDictionaryToJSIObject(runtime, cause));
return jsi::JSError(runtime, std::move(error));
}

/**
* Creates JS error value with current JS runtime and error details.
*/
static jsi::Value convertJSErrorDetailsToJSRuntimeError(jsi::Runtime &runtime, NSDictionary *jsErrorDetails)
static jsi::Value convertJSErrorDetailsToJSRuntimeError(jsi::Runtime &runtime, NSDictionary *jsErrorDetails, const std::optional<std::string> &jsInvocationStack)
{
NSString *message = jsErrorDetails[@"message"];

auto jsError = createJSRuntimeError(runtime, [message UTF8String]);
auto jsError = createJSRuntimeError(runtime, std::string([message UTF8String]));
jsError.asObject(runtime).setProperty(runtime, "cause", convertObjCObjectToJSIValue(runtime, jsErrorDetails));

if (jsInvocationStack.has_value()) {
jsError.asObject(runtime).setProperty(runtime, "stack", *jsInvocationStack);
}

for (NSString *key in jsErrorDetails) {
id value = jsErrorDetails[key];
jsError.asObject(runtime).setProperty(runtime, [key UTF8String], convertObjCObjectToJSIValue(runtime, value));
Expand All @@ -237,6 +258,16 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s

jsi::Function Promise = runtime.global().getPropertyAsFunction(runtime, "Promise");

// JS Stack at the time when the promise is created.
std::optional<std::string> jsInvocationStack;
if (RCTTraceTurboModulePromiseRejections()) {
jsInvocationStack = createJSRuntimeError(runtime, jsi::Value::undefined())
.asObject(runtime)
.getProperty(runtime, "stack")
.asString(runtime)
.utf8(runtime);
}

// Note: the passed invoke() block is not retained by default, so let's retain it here to help keep it longer.
// Otherwise, there's a risk of it getting released before the promise function below executes.
PromiseInvocationBlock invokeCopy = [invoke copy];
Expand All @@ -246,7 +277,7 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s
runtime,
jsi::PropNameID::forAscii(runtime, "fn"),
2,
[invokeCopy, jsInvoker = jsInvoker_, moduleName = name_, methodName](
[invokeCopy, jsInvoker = jsInvoker_, moduleName = name_, methodName, jsInvocationStack = std::move(jsInvocationStack)](
jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) {
std::string moduleMethod = moduleName + "." + methodName + "()";

Expand Down Expand Up @@ -296,8 +327,8 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s
}

NSDictionary *jsErrorDetails = RCTJSErrorFromCodeMessageAndNSError(code, message, error);
reject->call([jsErrorDetails](jsi::Runtime &rt, jsi::Function &jsFunction) {
jsFunction.call(rt, convertJSErrorDetailsToJSRuntimeError(rt, jsErrorDetails));
reject->call([jsErrorDetails, jsInvocationStack](jsi::Runtime &rt, jsi::Function &jsFunction) {
jsFunction.call(rt, convertJSErrorDetailsToJSRuntimeError(rt, jsErrorDetails, jsInvocationStack));
});
resolveWasCalled = NO;
resolve = std::nullopt;
Expand All @@ -309,6 +340,38 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s
}));
}

static NSError * maybeCatchException(
BOOL shoudCatch,
id causeOrError)
{
if (!shoudCatch) {
// Crash on native layer there is no promise in JS to reject.
// We executed JSFunction returning void asynchrounously.
throw;
}

if ([causeOrError isKindOfClass:[NSError class]]) {
return causeOrError;
}

if ([causeOrError isKindOfClass:[NSDictionary class]]) {
return [[NSError alloc] initWithDomain:RCTErrorDomain code:-1 userInfo:causeOrError];
}

if ([causeOrError isKindOfClass:[NSException class]]) {
NSException *exception = (NSException *)causeOrError;
return [[NSError alloc] initWithDomain:RCTErrorDomain code:-1 userInfo:@{
@"name": exception.name,
NSLocalizedDescriptionKey: exception.reason,
@"stackSymbols": exception.callStackSymbols,
@"stackReturnAddresses": exception.callStackReturnAddresses,
}];
}

// This should never happen, to avoid consequent errors, we wrapp the unknown value in NSError.
return [[NSError alloc] initWithDomain:RCTErrorDomain code:-1 userInfo:@{ @"unknown": causeOrError }];
}

/**
* Perform method invocation on a specific queue as configured by the module class.
* This serves as a backward-compatible support for RCTBridgeModule's methodQueue API.
Expand All @@ -323,7 +386,8 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s
bool isSync,
const char *methodName,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation)
NSMutableArray *retainedObjectsForInvocation,
_Nullable RCTPromiseRejectBlock reject)
{
__block id result;
__weak id<RCTBridgeModule> weakModule = instance_;
Expand All @@ -343,12 +407,46 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s
TurboModulePerfLogger::asyncMethodCallExecutionStart(moduleName, methodNameStr.c_str(), asyncCallCounter);
}

@try {
[inv invokeWithTarget:strongModule];
} @catch (NSException *exception) {
throw convertNSExceptionToJSError(runtime, exception);
} @finally {
[retainedObjectsForInvocation removeAllObjects];
NSError *caughtException = nil;
BOOL shouldCatchException = isSync || reject;
try {
@try {
[inv invokeWithTarget:strongModule];
} @catch (NSException *exception) {
caughtException = maybeCatchException(shouldCatchException, exception);
} @catch (NSError *error) {
caughtException = maybeCatchException(shouldCatchException, error);
} @catch (NSString *errorMessage) {
caughtException = maybeCatchException(shouldCatchException, @{
NSLocalizedDescriptionKey: errorMessage,
});
} @catch (id e) {
caughtException = maybeCatchException(shouldCatchException, @{
NSLocalizedDescriptionKey: @"Unknown Objective-C Object thrown.",
});
} @finally {
[retainedObjectsForInvocation removeAllObjects];
}
} catch (const std::exception &exception) {
caughtException = maybeCatchException(shouldCatchException, @{
NSLocalizedDescriptionKey: [NSString stringWithUTF8String:exception.what()],
});
} catch (const std::string &errorMessage) {
caughtException = maybeCatchException(shouldCatchException, @{
NSLocalizedDescriptionKey: [NSString stringWithUTF8String:errorMessage.c_str()],
});
} catch (...) {
caughtException = maybeCatchException(shouldCatchException, @{
NSLocalizedDescriptionKey: @"Unknown C++ exception thrown.",
});
}

if (caughtException) {
if (isSync) {
throw convertNSDictionaryToJSError(runtime, RCTJSErrorFromCodeMessageAndNSError(nil, nil, caughtException));
} else {
reject(nil, nil, caughtException);
}
}

if (!isSync) {
Expand Down Expand Up @@ -741,8 +839,17 @@ SystraceSection s(
[inv setArgument:(void *)&rejectCopy atIndex:count + 3];
[retainedObjectsForInvocation addObject:resolveCopy];
[retainedObjectsForInvocation addObject:rejectCopy];
RCTPromiseRejectBlock rejectOnNativeError = nil;
if (RCTRejectTurboModulePromiseOnNativeError()) {
rejectOnNativeError = rejectCopy;
};
// The return type becomes void in the ObjC side.
performMethodInvocation(runtime, isSyncInvocation, methodName, inv, retainedObjectsForInvocation);
performMethodInvocation(runtime,
isMethodSync(VoidKind),
methodName,
inv,
retainedObjectsForInvocation,
rejectOnNativeError);
});
break;
}
Expand All @@ -763,7 +870,7 @@ SystraceSection s(
case ObjectKind:
case ArrayKind:
case FunctionKind: {
id result = performMethodInvocation(runtime, true, methodName, inv, retainedObjectsForInvocation);
id result = performMethodInvocation(runtime, true, methodName, inv, retainedObjectsForInvocation, nil);
TurboModulePerfLogger::syncMethodCallReturnConversionStart(moduleName, methodName);
returnValue = convertReturnIdToJSIValue(runtime, methodName, returnType, result);
TurboModulePerfLogger::syncMethodCallReturnConversionEnd(moduleName, methodName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
- (void)voidFuncAssert;
- (NSDictionary *)getObjectAssert:(NSDictionary *)arg;
- (void)promiseAssert:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
- (NSDictionary *)getObjectCppThrows:(NSDictionary *)arg;
- (NSDictionary *)constantsToExport;
- (NSDictionary *)getConstants;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,16 @@
.invokeObjCMethod(rt, ObjectKind, "getConstants", @selector(getConstants), args, count);
}

static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getObjectCppThrows(
facebook::jsi::Runtime &rt,
TurboModule &turboModule,
const facebook::jsi::Value *args,
size_t count)
{
return static_cast<ObjCTurboModule &>(turboModule)
.invokeObjCMethod(rt, ObjectKind, "getObjectCppThrows", @selector(getObjectCppThrows:), args, count);
}

NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const ObjCTurboModule::InitParams &params)
: ObjCTurboModule(params)
{
Expand All @@ -223,6 +233,7 @@
methodMap_["voidFuncAssert"] = MethodMetadata{0, __hostFunction_NativeSampleTurboModuleSpecJSI_voidFuncAssert};
methodMap_["getObjectAssert"] = MethodMetadata{1, __hostFunction_NativeSampleTurboModuleSpecJSI_getObjectAssert};
methodMap_["promiseAssert"] = MethodMetadata{0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseAssert};
methodMap_["getObjectCppThrows"] = MethodMetadata{1, __hostFunction_NativeSampleTurboModuleSpecJSI_getObjectCppThrows};
methodMap_["getConstants"] = MethodMetadata{0, __hostFunction_NativeSampleTurboModuleSpecJSI_getConstants};
eventEmitterMap_["onPress"] = std::make_shared<AsyncEventEmitter<id>>();
eventEmitterMap_["onClick"] = std::make_shared<AsyncEventEmitter<id>>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ - (NSDictionary *)constantsToExport
@throw myException;
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, getObjectCppThrows : (NSDictionary *)arg)
{
throw std::runtime_error("Intentional error from Cpp getObjectCppThrows");
}

RCT_EXPORT_METHOD(voidFuncAssert)
{
RCTAssert(false, @"Intentional assert from ObjC voidFuncAssert");
Expand Down
Loading