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
This commit is contained in:
Ramanpreet Nara 2024-11-07 11:22:57 -08:00 committed by Facebook GitHub Bot
parent 791afd3209
commit 8b053a4fca
9 changed files with 107 additions and 36 deletions

View File

@ -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,

View File

@ -19,10 +19,10 @@ type Fn<Args, Return> = (...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,
) {
let _globalHandler: ErrorHandler =
global.RN$useAlwaysAvailableJSErrorHandling === true
? global.RN$handleException
: (e: mixed, isFatal: boolean) => {
throw e;
};

View File

@ -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)
//

View File

@ -10,6 +10,7 @@
'use strict';
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.
@ -31,3 +32,4 @@ if (!global.__fbDisableExceptionsManager) {
const ErrorUtils = require('../vendor/core/ErrorUtils');
ErrorUtils.setGlobalHandler(handleError);
}
}

View File

@ -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);
}

View File

@ -19,4 +19,5 @@ target_link_libraries(jserrorhandler
jsi
folly_runtime
mapbufferjni
react_featureflags
)

View File

@ -9,6 +9,7 @@
#include <cxxreact/ErrorUtils.h>
#include <glog/logging.h>
#include <react/bridging/Bridging.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <string>
#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,

View File

@ -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"

View File

@ -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,6 +465,9 @@ void ReactInstance::initializeRuntime(
}
auto isFatal = isTruthy(runtime, args[1]);
if (!ReactNativeFeatureFlags::
useAlwaysAvailableJSErrorHandling()) {
if (jsErrorHandler->isRuntimeReady()) {
if (isFatal) {
jsErrorHandler->notifyOfFatalError();
@ -451,6 +475,7 @@ void ReactInstance::initializeRuntime(
return jsi::Value(false);
}
}
auto jsError =
jsi::JSError(runtime, jsi::Value(runtime, args[0]));