diff --git a/packages/react-native/Libraries/LogBox/LogBox.js b/packages/react-native/Libraries/LogBox/LogBox.js index e9987147da7bc9..82930e7f34d29e 100644 --- a/packages/react-native/Libraries/LogBox/LogBox.js +++ b/packages/react-native/Libraries/LogBox/LogBox.js @@ -52,6 +52,17 @@ if (__DEV__) { isLogBoxInstalled = true; + if (global.RN$registerExceptionListener != null) { + global.RN$registerExceptionListener( + (error: ExtendedExceptionData & {preventDefault: () => mixed}) => { + if (!error.isFatal) { + error.preventDefault(); + addException(error); + } + }, + ); + } + // Trigger lazy initialization of module. require('../NativeModules/specs/NativeLogBox'); @@ -122,13 +133,15 @@ if (__DEV__) { } }, - addException(error: ExtendedExceptionData): void { - if (isLogBoxInstalled) { - LogBoxData.addException(error); - } - }, + addException, }; + function addException(error: ExtendedExceptionData): void { + if (isLogBoxInstalled) { + LogBoxData.addException(error); + } + } + const isRCTLogAdviceWarning = (...args: Array) => { // RCTLogAdvice is a native logging function designed to show users // a message in the console, but not show it to them in Logbox. diff --git a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp index 0caaf125ee10b1..73faee8870f0c5 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp +++ b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include using namespace facebook; @@ -35,10 +36,65 @@ bool isEmptyString(jsi::Runtime& runtime, const jsi::Value& value) { std::string stringifyToCpp(jsi::Runtime& runtime, const jsi::Value& value) { return value.toString(runtime).utf8(runtime); } + +bool isTruthy(jsi::Runtime& runtime, const jsi::Value& value) { + auto Boolean = runtime.global().getPropertyAsFunction(runtime, "Boolean"); + return Boolean.call(runtime, value).getBool(); +} } // namespace namespace facebook::react { +template <> +struct Bridging { + static jsi::Value toJs( + jsi::Runtime& runtime, + const JsErrorHandler::ParsedError::StackFrame& frame) { + auto stackFrame = jsi::Object(runtime); + auto file = bridging::toJs(runtime, frame.file, nullptr); + auto lineNumber = bridging::toJs(runtime, frame.lineNumber, nullptr); + auto column = bridging::toJs(runtime, frame.column, nullptr); + + stackFrame.setProperty(runtime, "file", file); + stackFrame.setProperty(runtime, "methodName", frame.methodName); + stackFrame.setProperty(runtime, "lineNumber", lineNumber); + stackFrame.setProperty(runtime, "column", column); + return stackFrame; + } +}; + +template <> +struct Bridging { + static jsi::Value toJs( + jsi::Runtime& runtime, + const JsErrorHandler::ParsedError& error) { + auto data = jsi::Object(runtime); + data.setProperty(runtime, "message", error.message); + data.setProperty( + runtime, + "originalMessage", + bridging::toJs(runtime, error.originalMessage, nullptr)); + data.setProperty( + runtime, "name", bridging::toJs(runtime, error.name, nullptr)); + data.setProperty( + runtime, + "componentStack", + bridging::toJs(runtime, error.componentStack, nullptr)); + + auto stack = jsi::Array(runtime, error.stack.size()); + for (size_t i = 0; i < error.stack.size(); i++) { + auto& frame = error.stack[i]; + stack.setValueAtIndex(runtime, i, bridging::toJs(runtime, frame)); + } + + data.setProperty(runtime, "stack", stack); + data.setProperty(runtime, "id", error.id); + data.setProperty(runtime, "isFatal", error.isFatal); + data.setProperty(runtime, "extraData", error.extraData); + return data; + } +}; + std::ostream& operator<<( std::ostream& os, const JsErrorHandler::ParsedError::StackFrame& frame) { @@ -96,12 +152,12 @@ void JsErrorHandler::handleError( jsi::JSError& error, bool isFatal) { // TODO: Current error parsing works and is stable. Can investigate using - // REGEX_HERMES to get additional Hermes data, though it requires JS setup. - if (isFatal) { - _hasHandledFatalError = true; - } - + // REGEX_HERMES to get additional Hermes data, though it requires JS setup if (_isRuntimeReady) { + if (isFatal) { + _hasHandledFatalError = true; + } + try { handleJSError(runtime, error, isFatal); return; @@ -114,6 +170,13 @@ void JsErrorHandler::handleError( } } + emitError(runtime, error, isFatal); +} + +void JsErrorHandler::emitError( + jsi::Runtime& runtime, + jsi::JSError& error, + bool isFatal) { auto message = error.getMessage(); auto errorObj = error.value().getObject(runtime); auto componentStackValue = errorObj.getProperty(runtime, "componentStack"); @@ -182,9 +245,48 @@ void JsErrorHandler::handleError( .extraData = std::move(extraData), }; + auto data = bridging::toJs(runtime, parsedError).asObject(runtime); + + auto isComponentError = + isTruthy(runtime, errorObj.getProperty(runtime, "isComponentError")); + data.setProperty(runtime, "isComponentError", isComponentError); + + std::shared_ptr shouldPreventDefault = std::make_shared(false); + auto preventDefault = jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "preventDefault"), + 0, + [shouldPreventDefault]( + jsi::Runtime& /*rt*/, + const jsi::Value& /*thisVal*/, + const jsi::Value* /*args*/, + size_t /*count*/) { + *shouldPreventDefault = true; + return jsi::Value::undefined(); + }); + + data.setProperty(runtime, "preventDefault", preventDefault); + + for (auto& errorListener : _errorListeners) { + errorListener(runtime, jsi::Value(runtime, data)); + } + + if (*shouldPreventDefault) { + return; + } + + if (isFatal) { + _hasHandledFatalError = true; + } + _onJsError(runtime, parsedError); } +void JsErrorHandler::registerErrorListener( + const std::function& errorListener) { + _errorListeners.push_back(errorListener); +} + bool JsErrorHandler::hasHandledFatalError() { return _hasHandledFatalError; } diff --git a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h index d46ba183f62adf..b8d784ac63eb97 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h +++ b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h @@ -45,6 +45,8 @@ class JsErrorHandler { void handleError(jsi::Runtime& runtime, jsi::JSError& error, bool isFatal); bool hasHandledFatalError(); + void registerErrorListener( + const std::function& listener); void setRuntimeReady(); bool isRuntimeReady(); void notifyOfFatalError(); @@ -60,6 +62,9 @@ class JsErrorHandler { OnJsError _onJsError; bool _hasHandledFatalError; bool _isRuntimeReady{}; + std::vector> _errorListeners; + + void emitError(jsi::Runtime& runtime, jsi::JSError& error, bool isFatal); }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec b/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec index cd500a553709e8..7d20104500197b 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec +++ b/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec @@ -51,6 +51,7 @@ Pod::Spec.new do |s| s.dependency "React-jsi" s.dependency "React-cxxreact" s.dependency "glog" + s.dependency "ReactCommon/turbomodule/bridging" add_dependency(s, "React-debug") if ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == "1" diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp index db90f4a60b1009..97c420ccf5b060 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp @@ -429,6 +429,47 @@ void ReactInstance::initializeRuntime( return jsi::Value(true); })); + defineReadOnlyGlobal( + runtime, + "RN$registerExceptionListener", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "registerExceptionListener"), + 1, + [errorListeners = std::vector>(), + jsErrorHandler = jsErrorHandler_]( + jsi::Runtime& runtime, + const jsi::Value& /*unused*/, + const jsi::Value* args, + size_t count) mutable { + if (count < 1) { + throw jsi::JSError( + runtime, + "registerExceptionListener: requires 1 argument: fn"); + } + + if (!args[0].isObject() || + !args[0].getObject(runtime).isFunction(runtime)) { + throw jsi::JSError( + runtime, + "registerExceptionListener: The first argument must be a function"); + } + + auto errorListener = std::make_shared( + args[0].getObject(runtime).getFunction(runtime)); + errorListeners.emplace_back(errorListener); + + jsErrorHandler->registerErrorListener( + [weakErrorListener = std::weak_ptr( + errorListener)](jsi::Runtime& runtime, jsi::Value data) { + if (auto strongErrorListener = weakErrorListener.lock()) { + strongErrorListener->call(runtime, data); + } + }); + + return jsi::Value::undefined(); + })); + defineReadOnlyGlobal( runtime, "RN$registerCallableModule", diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.h b/packages/react-native/ReactCommon/react/runtime/ReactInstance.h index 5c76e1956c0b05..0380e3185d698c 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.h +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.h @@ -17,6 +17,7 @@ #include #include #include +#include namespace facebook::react {