diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp index e8d9b4359cea..575842e6ee38 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp @@ -170,7 +170,7 @@ void RuntimeScheduler_Legacy::callExpiredTasks(jsi::Runtime& runtime) { executeTask(runtime, topPriorityTask, didUserCallbackTimeout); } } catch (jsi::JSError& error) { - onTaskError_(runtime, error); + reportError(runtime, error); } catch (std::exception& ex) { jsi::JSError error(runtime, std::string("Non-js exception: ") + ex.what()); onTaskError_(runtime, error); @@ -242,7 +242,7 @@ void RuntimeScheduler_Legacy::startWorkLoop(jsi::Runtime& runtime) { executeTask(runtime, topPriorityTask, didUserCallbackTimeout); } } catch (jsi::JSError& error) { - onTaskError_(runtime, error); + reportError(runtime, error); } catch (std::exception& ex) { jsi::JSError error(runtime, std::string("Non-js exception: ") + ex.what()); onTaskError_(runtime, error); @@ -252,6 +252,33 @@ void RuntimeScheduler_Legacy::startWorkLoop(jsi::Runtime& runtime) { isPerformingWork_ = false; } +void RuntimeScheduler_Legacy::reportError( + jsi::Runtime& runtime, + jsi::JSError& error) const { + try { + auto errorCtor = + runtime.global().getPropertyAsFunction(runtime, "Error"); + auto errorObj = + errorCtor + .callAsConstructor( + runtime, + jsi::String::createFromUtf8(runtime, error.getMessage())) + .getObject(runtime); + errorObj.setProperty( + runtime, + "stack", + jsi::String::createFromUtf8(runtime, error.getStack())); + jsi::JSError localError( + jsi::Value(std::move(errorObj)), + error.getMessage(), + error.getStack()); + onTaskError_(runtime, localError); + } catch (...) { + jsi::JSError fallbackError(runtime, error.getMessage()); + onTaskError_(runtime, fallbackError); + } +} + void RuntimeScheduler_Legacy::executeTask( jsi::Runtime& runtime, const std::shared_ptr& task, diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h index d5519bfcb4ef..065e008563aa 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h @@ -149,6 +149,8 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { void executeTask(jsi::Runtime &runtime, const std::shared_ptr &task, bool didUserCallbackTimeout); + void reportError(jsi::Runtime &runtime, jsi::JSError &error) const; + /* * Returns a time point representing the current point in time. May be called * from multiple threads. diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp index b786399ff556..59fad49b5698 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -380,13 +380,40 @@ void RuntimeScheduler_Modern::executeTask( task.callback = result.getObject(runtime).getFunction(runtime); } } catch (jsi::JSError& error) { - onTaskError_(runtime, error); + reportError(runtime, error); } catch (std::exception& ex) { jsi::JSError error(runtime, std::string("Non-js exception: ") + ex.what()); onTaskError_(runtime, error); } } +void RuntimeScheduler_Modern::reportError( + jsi::Runtime& runtime, + jsi::JSError& error) const { + try { + auto errorCtor = + runtime.global().getPropertyAsFunction(runtime, "Error"); + auto errorObj = + errorCtor + .callAsConstructor( + runtime, + jsi::String::createFromUtf8(runtime, error.getMessage())) + .getObject(runtime); + errorObj.setProperty( + runtime, + "stack", + jsi::String::createFromUtf8(runtime, error.getStack())); + jsi::JSError localError( + jsi::Value(std::move(errorObj)), + error.getMessage(), + error.getStack()); + onTaskError_(runtime, localError); + } catch (...) { + jsi::JSError fallbackError(runtime, error.getMessage()); + onTaskError_(runtime, fallbackError); + } +} + /** * This is partially equivalent to the "Perform a microtask checkpoint" step in * the Web event loop. See @@ -419,7 +446,7 @@ void RuntimeScheduler_Modern::performMicrotaskCheckpoint( break; } } catch (jsi::JSError& error) { - onTaskError_(runtime, error); + reportError(runtime, error); } catch (std::exception& ex) { jsi::JSError error( runtime, std::string("Non-js exception: ") + ex.what()); diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h index 05ba08896f19..6c38a63b6421 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -179,6 +179,16 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { void executeTask(jsi::Runtime &runtime, Task &task, bool didUserCallbackTimeout) const; + /* + * Re-creates a jsi::JSError as a proper Error instance in the given runtime + * and forwards it to onTaskError_. This is necessary because the caught + * error's jsi::Value may belong to a different jsi::Runtime (e.g. when the + * error originates from a background Hermes instance). Using a cross-runtime + * Value is undefined behavior and causes LogBox to show "Unknown". + * getMessage() and getStack() are plain C++ strings and always safe to use. + */ + void reportError(jsi::Runtime &runtime, jsi::JSError &error) const; + void updateRendering(HighResTimeStamp taskEndTime); bool performingMicrotaskCheckpoint_{false}; diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp index f04ad51bffc6..0b65b59834e5 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp @@ -1206,6 +1206,55 @@ TEST_P(RuntimeSchedulerTest, errorInTaskShouldNotStopMicrotasks) { EXPECT_EQ(stubErrorUtils_->getReportFatalCallCount(), 1); } +TEST_P(RuntimeSchedulerTest, handlingCrossRuntimeError) { + auto secondRuntime = facebook::hermes::makeHermesRuntime(); + + bool didRunTask = false; + auto callback = createHostFunctionFromLambda( + [&didRunTask, &secondRuntime](bool /*unused*/) { + didRunTask = true; + throw jsi::JSError(*secondRuntime, "Cross-runtime error"); + return jsi::Value::undefined(); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, std::move(callback)); + + EXPECT_FALSE(didRunTask); + EXPECT_EQ(stubQueue_->size(), 1); + + stubQueue_->tick(); + + EXPECT_TRUE(didRunTask); + EXPECT_EQ(stubQueue_->size(), 0); + EXPECT_EQ(stubErrorUtils_->getReportFatalCallCount(), 1); + EXPECT_EQ(stubErrorUtils_->getLastReportedMessage(), "Cross-runtime error"); +} + +TEST_P(RuntimeSchedulerTest, handlingErrorPreservesMessage) { + bool didRunTask = false; + auto callback = + createHostFunctionFromLambda([this, &didRunTask](bool /*unused*/) { + didRunTask = true; + throw jsi::JSError(*runtime_, "Same-runtime error"); + return jsi::Value::undefined(); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, std::move(callback)); + + EXPECT_FALSE(didRunTask); + EXPECT_EQ(stubQueue_->size(), 1); + + stubQueue_->tick(); + + EXPECT_TRUE(didRunTask); + EXPECT_EQ(stubQueue_->size(), 0); + EXPECT_EQ(stubErrorUtils_->getReportFatalCallCount(), 1); + EXPECT_EQ( + stubErrorUtils_->getLastReportedMessage(), "Same-runtime error"); +} + TEST_P(RuntimeSchedulerTest, reportsLongTasks) { // Only for event loop if (!GetParam()) { diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/StubErrorUtils.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/StubErrorUtils.h index 4d82dc1df680..4704722badf4 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/StubErrorUtils.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/StubErrorUtils.h @@ -46,8 +46,15 @@ class StubErrorUtils : public jsi::HostObject { name, 1, [this]( - jsi::Runtime &runtime, const jsi::Value &, const jsi::Value *arguments, size_t) noexcept -> jsi::Value { + jsi::Runtime &runtime, const jsi::Value &, const jsi::Value *arguments, size_t count) noexcept -> jsi::Value { reportFatalCallCount_++; + if (count > 0 && arguments[0].isObject()) { + auto obj = arguments[0].getObject(runtime); + auto msgVal = obj.getProperty(runtime, "message"); + if (msgVal.isString()) { + lastReportedMessage_ = msgVal.getString(runtime).utf8(runtime); + } + } return jsi::Value::undefined(); }); } @@ -60,8 +67,14 @@ class StubErrorUtils : public jsi::HostObject { return reportFatalCallCount_; } + const std::string &getLastReportedMessage() const + { + return lastReportedMessage_; + } + private: int reportFatalCallCount_; + std::string lastReportedMessage_; }; } // namespace facebook::react