Convert to JSException only NSException from sync methods (#50193)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50193

This fix makes sure that we convert to JSException only NSException thrwn by sync methods.
Currently, nothing in the stack will be capable of understanding that js error if it is triggered by an exception raised by an asyc method.

See https://github.com/reactwg/react-native-new-architecture/discussions/276 for further details

We need to cherry pick this in 0.78 and 0.79

## Changelog:
[iOS][Fixed] - Make sure the TM infra does not crash on NSException when triggered by async method

Reviewed By: fabriziocucci

Differential Revision: D71619229

fbshipit-source-id: b87aef5dd2720a2641c8da0904da651866370dc6
This commit is contained in:
Riccardo Cipolleschi
2025-03-24 14:15:26 +00:00
committed by Fabrizio Cucci
parent ffe7bd1471
commit 8eec35f134
@@ -57,7 +57,7 @@ static jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, NSNumber *va
static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value)
{
return jsi::String::createFromUtf8(runtime, [value UTF8String] ?: "");
return jsi::String::createFromUtf8(runtime, [value UTF8String] ? [value UTF8String] : "");
}
static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value)
@@ -195,7 +195,11 @@ static jsi::Value createJSRuntimeError(jsi::Runtime &runtime, const std::string
/**
* Creates JSError with current JS runtime and NSException stack trace.
*/
static jsi::JSError convertNSExceptionToJSError(jsi::Runtime &runtime, NSException *exception)
static jsi::JSError convertNSExceptionToJSError(
jsi::Runtime &runtime,
NSException *exception,
const std::string &moduleName,
const std::string &methodName)
{
std::string reason = [exception.reason UTF8String];
@@ -206,7 +210,8 @@ static jsi::JSError convertNSExceptionToJSError(jsi::Runtime &runtime, NSExcepti
cause.setProperty(
runtime, "stackReturnAddresses", convertNSArrayToJSIArray(runtime, exception.callStackReturnAddresses));
jsi::Value error = createJSRuntimeError(runtime, "Exception in HostFunction: " + reason);
std::string message = moduleName + "." + methodName + " raised an exception: " + reason;
jsi::Value error = createJSRuntimeError(runtime, message);
error.asObject(runtime).setProperty(runtime, "cause", std::move(cause));
return {runtime, std::move(error)};
}
@@ -338,28 +343,34 @@ id ObjCTurboModule::performMethodInvocation(
}
if (isSync) {
TurboModulePerfLogger::syncMethodCallExecutionStart(moduleName, methodNameStr.c_str());
TurboModulePerfLogger::syncMethodCallExecutionStart(moduleName, methodName);
} else {
TurboModulePerfLogger::asyncMethodCallExecutionStart(moduleName, methodNameStr.c_str(), asyncCallCounter);
TurboModulePerfLogger::asyncMethodCallExecutionStart(moduleName, methodName, asyncCallCounter);
}
@try {
[inv invokeWithTarget:strongModule];
} @catch (NSException *exception) {
throw convertNSExceptionToJSError(runtime, exception);
if (isSync) {
// We can only convert NSException to JSError in sync method calls.
// See https://github.com/reactwg/react-native-new-architecture/discussions/276#discussioncomment-12567155
throw convertNSExceptionToJSError(runtime, exception, std::string{moduleName}, methodNameStr);
} else {
@throw exception;
}
} @finally {
[retainedObjectsForInvocation removeAllObjects];
}
if (!isSync) {
TurboModulePerfLogger::asyncMethodCallExecutionEnd(moduleName, methodNameStr.c_str(), asyncCallCounter);
TurboModulePerfLogger::asyncMethodCallExecutionEnd(moduleName, methodName, asyncCallCounter);
return;
}
void *rawResult;
[inv getReturnValue:&rawResult];
result = (__bridge id)rawResult;
TurboModulePerfLogger::syncMethodCallExecutionEnd(moduleName, methodNameStr.c_str());
TurboModulePerfLogger::syncMethodCallExecutionEnd(moduleName, methodName);
};
if (isSync) {
@@ -401,23 +412,23 @@ void ObjCTurboModule::performVoidMethodInvocation(
}
if (shouldVoidMethodsExecuteSync_) {
TurboModulePerfLogger::syncMethodCallExecutionStart(moduleName, methodNameStr.c_str());
TurboModulePerfLogger::syncMethodCallExecutionStart(moduleName, methodName);
} else {
TurboModulePerfLogger::asyncMethodCallExecutionStart(moduleName, methodNameStr.c_str(), asyncCallCounter);
TurboModulePerfLogger::asyncMethodCallExecutionStart(moduleName, methodName, asyncCallCounter);
}
@try {
[inv invokeWithTarget:strongModule];
} @catch (NSException *exception) {
throw convertNSExceptionToJSError(runtime, exception);
throw convertNSExceptionToJSError(runtime, exception, std::string{moduleName}, methodNameStr);
} @finally {
[retainedObjectsForInvocation removeAllObjects];
}
if (shouldVoidMethodsExecuteSync_) {
TurboModulePerfLogger::syncMethodCallExecutionEnd(moduleName, methodNameStr.c_str());
TurboModulePerfLogger::syncMethodCallExecutionEnd(moduleName, methodName);
} else {
TurboModulePerfLogger::asyncMethodCallExecutionEnd(moduleName, methodNameStr.c_str(), asyncCallCounter);
TurboModulePerfLogger::asyncMethodCallExecutionEnd(moduleName, methodName, asyncCallCounter);
}
return;