From 8b053a4fcac857b8c8e664d10b92d1ea71091bfc Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Thu, 7 Nov 2024 11:22:57 -0800 Subject: [PATCH] Implement always available js error handling (#47466) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/47466 Now, when the useAlwaysAvailableJSErrorHandling feature flag is true, React Native will use the earlyjs c++ error reporting pipeline for handling all javascript errors! Changelog: [Internal] Reviewed By: javache Differential Revision: D64715159 fbshipit-source-id: 597a5278eb792f87dca10e06fa9816b3a8c47b84 --- packages/polyfills/console.js | 42 +++++++++++++++++++ packages/polyfills/error-guard.js | 12 +++--- .../Libraries/Core/ExceptionsManager.js | 5 +-- .../Libraries/Core/setUpErrorHandling.js | 38 +++++++++-------- .../react-native/Libraries/LogBox/LogBox.js | 2 +- .../ReactCommon/jserrorhandler/CMakeLists.txt | 1 + .../jserrorhandler/JsErrorHandler.cpp | 7 +++- .../React-jserrorhandler.podspec | 1 + .../react/runtime/ReactInstance.cpp | 35 +++++++++++++--- 9 files changed, 107 insertions(+), 36 deletions(-) diff --git a/packages/polyfills/console.js b/packages/polyfills/console.js index 8010aa163b9..22e4ffdc0aa 100644 --- a/packages/polyfills/console.js +++ b/packages/polyfills/console.js @@ -553,6 +553,48 @@ if (global.nativeLoggingHook) { assert: consoleAssertPolyfill, }; + // TODO(T206796580): This was copy-pasted from ExceptionsManager.js + // Delete the copy there after the c++ pipeline is rolled out everywhere. + if (global.RN$useAlwaysAvailableJSErrorHandling === true) { + let originalConsoleError = console.error; + console.reportErrorsAsExceptions = true; + function stringifySafe(arg) { + return inspect(arg, {depth: 10}).replaceAll(/\n\s*/g, ' '); + } + console.error = function (...args) { + originalConsoleError.apply(this, args); + if (!console.reportErrorsAsExceptions) { + return; + } + if (global.RN$inExceptionHandler?.()) { + return; + } + let error; + + const firstArg = args[0]; + if (firstArg?.stack) { + // RN$handleException will console.error this with high enough fidelity. + error = firstArg; + } else { + if (typeof firstArg === 'string' && firstArg.startsWith('Warning: ')) { + // React warnings use console.error so that a stack trace is shown, but + // we don't (currently) want these to show a redbox + return; + } + const message = args + .map(arg => (typeof arg === 'string' ? arg : stringifySafe(arg))) + .join(' '); + + error = new Error(message); + error.name = 'console.error'; + } + + const isFatal = false; + const reportToConsole = false; + global.RN$handleException(error, isFatal, reportToConsole); + }; + } + Object.defineProperty(console, '_isPolyfilled', { value: true, enumerable: false, diff --git a/packages/polyfills/error-guard.js b/packages/polyfills/error-guard.js index 389b5a20f81..652efeb5abd 100644 --- a/packages/polyfills/error-guard.js +++ b/packages/polyfills/error-guard.js @@ -19,12 +19,12 @@ type Fn = (...Args) => Return; * when loading a module. This will report any errors encountered before * ExceptionsManager is configured. */ -let _globalHandler: ErrorHandler = function onError( - e: mixed, - isFatal: boolean, -) { - throw e; -}; +let _globalHandler: ErrorHandler = + global.RN$useAlwaysAvailableJSErrorHandling === true + ? global.RN$handleException + : (e: mixed, isFatal: boolean) => { + throw e; + }; /** * The particular require runtime that we are using looks for a global diff --git a/packages/react-native/Libraries/Core/ExceptionsManager.js b/packages/react-native/Libraries/Core/ExceptionsManager.js index 019c3eff95c..a0b22359ea0 100644 --- a/packages/react-native/Libraries/Core/ExceptionsManager.js +++ b/packages/react-native/Libraries/Core/ExceptionsManager.js @@ -177,10 +177,7 @@ function reactConsoleErrorHandler(...args) { if (!console.reportErrorsAsExceptions) { return; } - if ( - inExceptionHandler || - (global.RN$inExceptionHandler && global.RN$inExceptionHandler()) - ) { + if (inExceptionHandler || global.RN$inExceptionHandler?.()) { // The fundamental trick here is that are multiple entry point to logging errors: // (see D19743075 for more background) // diff --git a/packages/react-native/Libraries/Core/setUpErrorHandling.js b/packages/react-native/Libraries/Core/setUpErrorHandling.js index 32846d42f83..ffbfb618b42 100644 --- a/packages/react-native/Libraries/Core/setUpErrorHandling.js +++ b/packages/react-native/Libraries/Core/setUpErrorHandling.js @@ -10,24 +10,26 @@ 'use strict'; -/** - * Sets up the console and exception handling (redbox) for React Native. - * You can use this module directly, or just require InitializeCore. - */ -const ExceptionsManager = require('./ExceptionsManager'); -ExceptionsManager.installConsoleErrorReporter(); +if (global.RN$useAlwaysAvailableJSErrorHandling !== true) { + /** + * Sets up the console and exception handling (redbox) for React Native. + * You can use this module directly, or just require InitializeCore. + */ + const ExceptionsManager = require('./ExceptionsManager'); + ExceptionsManager.installConsoleErrorReporter(); -// Set up error handler -if (!global.__fbDisableExceptionsManager) { - const handleError = (e: mixed, isFatal: boolean) => { - try { - ExceptionsManager.handleException(e, isFatal); - } catch (ee) { - console.log('Failed to print error: ', ee.message); - throw e; - } - }; + // Set up error handler + if (!global.__fbDisableExceptionsManager) { + const handleError = (e: mixed, isFatal: boolean) => { + try { + ExceptionsManager.handleException(e, isFatal); + } catch (ee) { + console.log('Failed to print error: ', ee.message); + throw e; + } + }; - const ErrorUtils = require('../vendor/core/ErrorUtils'); - ErrorUtils.setGlobalHandler(handleError); + const ErrorUtils = require('../vendor/core/ErrorUtils'); + ErrorUtils.setGlobalHandler(handleError); + } } diff --git a/packages/react-native/Libraries/LogBox/LogBox.js b/packages/react-native/Libraries/LogBox/LogBox.js index 82930e7f34d..473f1e1a5b2 100644 --- a/packages/react-native/Libraries/LogBox/LogBox.js +++ b/packages/react-native/Libraries/LogBox/LogBox.js @@ -55,7 +55,7 @@ if (__DEV__) { if (global.RN$registerExceptionListener != null) { global.RN$registerExceptionListener( (error: ExtendedExceptionData & {preventDefault: () => mixed}) => { - if (!error.isFatal) { + if (global.RN$isRuntimeReady?.() || !error.isFatal) { error.preventDefault(); addException(error); } diff --git a/packages/react-native/ReactCommon/jserrorhandler/CMakeLists.txt b/packages/react-native/ReactCommon/jserrorhandler/CMakeLists.txt index 71ee2630328..b05009bd70b 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/CMakeLists.txt +++ b/packages/react-native/ReactCommon/jserrorhandler/CMakeLists.txt @@ -19,4 +19,5 @@ target_link_libraries(jserrorhandler jsi folly_runtime mapbufferjni + react_featureflags ) diff --git a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp index 7c19502c334..904baff427d 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 #include "StackTraceParser.h" @@ -228,7 +229,9 @@ void JsErrorHandler::handleError( 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) { + + if (!ReactNativeFeatureFlags::useAlwaysAvailableJSErrorHandling() && + _isRuntimeReady) { if (isFatal) { _hasHandledFatalError = true; } @@ -325,7 +328,7 @@ void JsErrorHandler::handleErrorWithCppPipeline( auto id = nextExceptionId(); ParsedError parsedError = { - .message = "EarlyJsError: " + message, + .message = _isRuntimeReady ? message : ("EarlyJsError: " + message), .originalMessage = originalMessage, .name = name, .componentStack = componentStack, diff --git a/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec b/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec index da6c75c6838..d2568852351 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec +++ b/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec @@ -52,6 +52,7 @@ Pod::Spec.new do |s| s.dependency "React-cxxreact" s.dependency "glog" s.dependency "ReactCommon/turbomodule/bridging" + add_dependency(s, "React-featureflags") 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 20a268761a5..bb19ff78d8b 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp @@ -409,6 +409,27 @@ void ReactInstance::initializeRuntime( defineReactInstanceFlags(runtime, options); + defineReadOnlyGlobal( + runtime, + "RN$useAlwaysAvailableJSErrorHandling", + jsi::Value( + ReactNativeFeatureFlags::useAlwaysAvailableJSErrorHandling())); + + defineReadOnlyGlobal( + runtime, + "RN$isRuntimeReady", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "isRuntimeReady"), + 0, + [jsErrorHandler = jsErrorHandler_]( + jsi::Runtime& /*runtime*/, + const jsi::Value& /*unused*/, + const jsi::Value* /*args*/, + size_t /*count*/) { + return jsErrorHandler->isRuntimeReady(); + })); + defineReadOnlyGlobal( runtime, "RN$inExceptionHandler", @@ -444,12 +465,16 @@ void ReactInstance::initializeRuntime( } auto isFatal = isTruthy(runtime, args[1]); - if (jsErrorHandler->isRuntimeReady()) { - if (isFatal) { - jsErrorHandler->notifyOfFatalError(); - } - return jsi::Value(false); + if (!ReactNativeFeatureFlags:: + useAlwaysAvailableJSErrorHandling()) { + if (jsErrorHandler->isRuntimeReady()) { + if (isFatal) { + jsErrorHandler->notifyOfFatalError(); + } + + return jsi::Value(false); + } } auto jsError =