diff --git a/packages/react-native/Libraries/Core/ExceptionsManager.js b/packages/react-native/Libraries/Core/ExceptionsManager.js index 62ea6343d7ef1f..019c3eff95cca7 100644 --- a/packages/react-native/Libraries/Core/ExceptionsManager.js +++ b/packages/react-native/Libraries/Core/ExceptionsManager.js @@ -142,9 +142,10 @@ let inExceptionHandler = false; */ function handleException(e: mixed, isFatal: boolean) { // TODO(T196834299): We should really use a c++ turbomodule for this + const reportToConsole = true; if ( !global.RN$handleException || - !global.RN$handleException(e, isFatal) + !global.RN$handleException(e, isFatal, reportToConsole) ) { let error: Error; if (e instanceof Error) { @@ -161,7 +162,7 @@ function handleException(e: mixed, isFatal: boolean) { /* $FlowFixMe[class-object-subtyping] added when improving typing for this * parameters */ // $FlowFixMe[incompatible-call] - reportException(error, isFatal, /*reportToConsole*/ true); + reportException(error, isFatal, reportToConsole); } finally { inExceptionHandler = false; } @@ -176,7 +177,10 @@ function reactConsoleErrorHandler(...args) { if (!console.reportErrorsAsExceptions) { return; } - if (inExceptionHandler) { + if ( + inExceptionHandler || + (global.RN$inExceptionHandler && global.RN$inExceptionHandler()) + ) { // The fundamental trick here is that are multiple entry point to logging errors: // (see D19743075 for more background) // @@ -230,14 +234,21 @@ function reactConsoleErrorHandler(...args) { error.name = 'console.error'; } - reportException( - /* $FlowFixMe[class-object-subtyping] added when improving typing for this - * parameters */ - // $FlowFixMe[incompatible-call] - error, - false, // isFatal - false, // reportToConsole - ); + const isFatal = false; + const reportToConsole = false; + if ( + !global.RN$handleException || + !global.RN$handleException(error, isFatal, reportToConsole) + ) { + reportException( + /* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ + // $FlowFixMe[incompatible-call] + error, + isFatal, + reportToConsole, + ); + } } /** diff --git a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp index 69652a173caaa7..2c156e6e43126d 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp +++ b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp @@ -62,6 +62,22 @@ jsi::Object wrapInErrorIfNecessary( : Error.callAsConstructor(runtime, value).getObject(runtime); return error; } + +class SetFalseOnDestruct { + std::shared_ptr _value; + + public: + SetFalseOnDestruct(const SetFalseOnDestruct&) = delete; + SetFalseOnDestruct& operator=(const SetFalseOnDestruct&) = delete; + SetFalseOnDestruct(SetFalseOnDestruct&&) = delete; + SetFalseOnDestruct& operator=(SetFalseOnDestruct&&) = delete; + explicit SetFalseOnDestruct(std::shared_ptr value) + : _value(std::move(value)) {} + ~SetFalseOnDestruct() { + *_value = false; + } +}; + } // namespace namespace facebook::react { @@ -162,7 +178,7 @@ std::ostream& operator<<( JsErrorHandler::JsErrorHandler(JsErrorHandler::OnJsError onJsError) : _onJsError(std::move(onJsError)), - _hasHandledFatalError(false){ + _inErrorHandler(std::make_shared(false)){ }; @@ -171,7 +187,8 @@ JsErrorHandler::~JsErrorHandler() {} void JsErrorHandler::handleError( jsi::Runtime& runtime, jsi::JSError& error, - bool isFatal) { + bool isFatal, + bool logToConsole) { // TODO: Current error parsing works and is stable. Can investigate using // REGEX_HERMES to get additional Hermes data, though it requires JS setup if (_isRuntimeReady) { @@ -191,13 +208,17 @@ void JsErrorHandler::handleError( } } - emitError(runtime, error, isFatal); + handleErrorWithCppPipeline(runtime, error, isFatal, logToConsole); } -void JsErrorHandler::emitError( +void JsErrorHandler::handleErrorWithCppPipeline( jsi::Runtime& runtime, jsi::JSError& error, - bool isFatal) { + bool isFatal, + bool logToConsole) { + *_inErrorHandler = true; + SetFalseOnDestruct temp{_inErrorHandler}; + auto message = error.getMessage(); auto errorObj = wrapInErrorIfNecessary(runtime, error.value()); auto componentStackValue = errorObj.getProperty(runtime, "componentStack"); @@ -278,6 +299,14 @@ void JsErrorHandler::emitError( isTruthy(runtime, errorObj.getProperty(runtime, "isComponentError")); data.setProperty(runtime, "isComponentError", isComponentError); + if (logToConsole) { + auto console = runtime.global().getPropertyAsObject(runtime, "console"); + auto errorFn = console.getPropertyAsFunction(runtime, "error"); + auto finalMessage = + jsi::String::createFromUtf8(runtime, parsedError.message); + errorFn.callWithThis(runtime, console, finalMessage); + } + std::shared_ptr shouldPreventDefault = std::make_shared(false); auto preventDefault = jsi::Function::createFromHostFunction( runtime, @@ -330,4 +359,8 @@ void JsErrorHandler::notifyOfFatalError() { _hasHandledFatalError = true; } +bool JsErrorHandler::inErrorHandler() { + return *_inErrorHandler; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h index b8d784ac63eb97..8d6f2c6467586c 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h +++ b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h @@ -43,13 +43,18 @@ class JsErrorHandler { explicit JsErrorHandler(OnJsError onJsError); ~JsErrorHandler(); - void handleError(jsi::Runtime& runtime, jsi::JSError& error, bool isFatal); + void handleError( + jsi::Runtime& runtime, + jsi::JSError& error, + bool isFatal, + bool logToConsole = true); bool hasHandledFatalError(); void registerErrorListener( const std::function& listener); void setRuntimeReady(); bool isRuntimeReady(); void notifyOfFatalError(); + bool inErrorHandler(); private: /** @@ -62,9 +67,14 @@ class JsErrorHandler { OnJsError _onJsError; bool _hasHandledFatalError; bool _isRuntimeReady{}; + std::shared_ptr _inErrorHandler; std::vector> _errorListeners; - void emitError(jsi::Runtime& runtime, jsi::JSError& error, bool isFatal); + void handleErrorWithCppPipeline( + jsi::Runtime& runtime, + jsi::JSError& error, + bool isFatal, + bool logToConsole); }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp index 15ac4c8465c3a3..20a268761a5dee 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp @@ -409,6 +409,21 @@ void ReactInstance::initializeRuntime( defineReactInstanceFlags(runtime, options); + defineReadOnlyGlobal( + runtime, + "RN$inExceptionHandler", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "inExceptionHandler"), + 0, + [jsErrorHandler = jsErrorHandler_]( + jsi::Runtime& /*runtime*/, + const jsi::Value& /*unused*/, + const jsi::Value* /*args*/, + size_t /*count*/) { + return jsErrorHandler->inErrorHandler(); + })); + // TODO(T196834299): We should really use a C++ turbomodule for this defineReadOnlyGlobal( runtime, @@ -416,7 +431,7 @@ void ReactInstance::initializeRuntime( jsi::Function::createFromHostFunction( runtime, jsi::PropNameID::forAscii(runtime, "handleException"), - 2, + 3, [jsErrorHandler = jsErrorHandler_]( jsi::Runtime& runtime, const jsi::Value& /*unused*/, @@ -425,7 +440,7 @@ void ReactInstance::initializeRuntime( if (count < 2) { throw jsi::JSError( runtime, - "handleException requires 2 arguments: error, isFatal"); + "handleException requires 3 arguments: error, isFatal, logToConsole (optional)"); } auto isFatal = isTruthy(runtime, args[1]); @@ -439,7 +454,14 @@ void ReactInstance::initializeRuntime( auto jsError = jsi::JSError(runtime, jsi::Value(runtime, args[0])); - jsErrorHandler->handleError(runtime, jsError, isFatal); + + if (count == 2) { + jsErrorHandler->handleError(runtime, jsError, isFatal); + } else { + auto logToConsole = isTruthy(runtime, args[2]); + jsErrorHandler->handleError( + runtime, jsError, isFatal, logToConsole); + } return jsi::Value(true); }));