Files
react-native/Libraries/Core/ExceptionsManager.js
T
Luna Wei b095432e6d Catch getRouteEntry errors
Summary:
## Changelog:

[Internal][Changed] - Prevent symbolicating stacktrace and no logbox when running in express route

Context:
ExpressRoute doesn't support some things (like promises) due to the limited initialization we do.

Right now the app will crash when trying to evaluate those entrypoints in express route if they depend on such initialization. Ideally a redbox would warn the developer that express route would break if they modify their express-route compatible entrypoint. Displaying a redbox seems to require a bit of refactoring as it can't easily be triggered from native/express-route -- something more to investigate. Occasionally one does appear (when trying the attached test plan) but it is inconsistent and seems dependent on timing of bridge, express route initialization.

The plan:
* Since we are going to roll out an opt-in for each surface (note there are two flags, `fetchWithExpressRouteIfAvailable` and `useExpressRouteIfInitialized` - the former being for using `getPreloadProps` to parallel fetch and the latter as a flag to get route information) we have more control of the roll out of express route.

Things still to improve:
* It's obviously not great that we don't get better errors -- something to address if that is really the next blocker to rolling out ExpressRoute

Reviewed By: sahrens, ejanzer

Differential Revision: D22026444

fbshipit-source-id: 7698109f5921f82a2d0bc9a8346e12b67defca27
2020-06-18 16:58:21 -07:00

268 lines
8.6 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import type {ExtendedError} from './Devtools/parseErrorStack';
import type {ExceptionData} from './NativeExceptionsManager';
class SyntheticError extends Error {
name: string = '';
}
type ExceptionDecorator = ExceptionData => ExceptionData;
let userExceptionDecorator: ?ExceptionDecorator;
let inUserExceptionDecorator = false;
/**
* Allows the app to add information to the exception report before it is sent
* to native. This API is not final.
*/
function unstable_setExceptionDecorator(
exceptionDecorator: ?ExceptionDecorator,
) {
userExceptionDecorator = exceptionDecorator;
}
function preprocessException(data: ExceptionData): ExceptionData {
if (userExceptionDecorator && !inUserExceptionDecorator) {
inUserExceptionDecorator = true;
try {
return userExceptionDecorator(data);
} catch {
// Fall through
} finally {
inUserExceptionDecorator = false;
}
}
return data;
}
/**
* Handles the developer-visible aspect of errors and exceptions
*/
let exceptionID = 0;
function reportException(
e: ExtendedError,
isFatal: boolean,
reportToConsole: boolean, // only true when coming from handleException; the error has not yet been logged
) {
const NativeExceptionsManager = require('./NativeExceptionsManager').default;
if (NativeExceptionsManager) {
const parseErrorStack = require('./Devtools/parseErrorStack');
const stack = parseErrorStack(e);
const currentExceptionID = ++exceptionID;
const originalMessage = e.message || '';
let message = originalMessage;
if (e.componentStack != null) {
message += `\n\nThis error is located at:${e.componentStack}`;
}
const namePrefix = e.name == null || e.name === '' ? '' : `${e.name}: `;
if (!message.startsWith(namePrefix)) {
message = namePrefix + message;
}
message =
e.jsEngine == null ? message : `${message}, js engine: ${e.jsEngine}`;
const isHandledByLogBox =
e.forceRedbox !== true && !global.RN$Bridgeless && !global.RN$Express;
const data = preprocessException({
message,
originalMessage: message === originalMessage ? null : originalMessage,
name: e.name == null || e.name === '' ? null : e.name,
componentStack:
typeof e.componentStack === 'string' ? e.componentStack : null,
stack,
id: currentExceptionID,
isFatal,
extraData: {
jsEngine: e.jsEngine,
rawStack: e.stack,
// Hack to hide native redboxes when in the LogBox experiment.
// This is intentionally untyped and stuffed here, because it is temporary.
suppressRedBox: isHandledByLogBox,
},
});
if (reportToConsole) {
// we feed back into console.error, to make sure any methods that are
// monkey patched on top of console.error are called when coming from
// handleException
console.error(data.message);
}
if (__DEV__ && isHandledByLogBox) {
const LogBoxData = require('../LogBox/Data/LogBoxData');
LogBoxData.addException({
...data,
isComponentError: !!e.isComponentError,
});
}
NativeExceptionsManager.reportException(data);
if (__DEV__ && !global.RN$Express) {
if (e.preventSymbolication === true) {
return;
}
const symbolicateStackTrace = require('./Devtools/symbolicateStackTrace');
symbolicateStackTrace(stack)
.then(({stack: prettyStack}) => {
if (prettyStack) {
NativeExceptionsManager.updateExceptionMessage(
data.message,
prettyStack,
currentExceptionID,
);
} else {
throw new Error('The stack is null');
}
})
.catch(error => {
console.log('Unable to symbolicate stack trace: ' + error.message);
});
}
} else if (reportToConsole) {
// we feed back into console.error, to make sure any methods that are
// monkey patched on top of console.error are called when coming from
// handleException
console.error(e);
}
}
declare var console: typeof console & {
_errorOriginal: typeof console.error,
reportErrorsAsExceptions: boolean,
...
};
// If we trigger console.error _from_ handleException,
// we do want to make sure that console.error doesn't trigger error reporting again
let inExceptionHandler = false;
/**
* Logs exceptions to the (native) console and displays them
*/
function handleException(e: mixed, isFatal: boolean) {
let error: Error;
if (e instanceof Error) {
error = e;
} else {
// Workaround for reporting errors caused by `throw 'some string'`
// Unfortunately there is no way to figure out the stacktrace in this
// case, so if you ended up here trying to trace an error, look for
// `throw '<error message>'` somewhere in your codebase.
error = new SyntheticError(e);
}
try {
inExceptionHandler = true;
reportException(error, isFatal, /*reportToConsole*/ true);
} finally {
inExceptionHandler = false;
}
}
function reactConsoleErrorHandler() {
// bubble up to any original handlers
console._errorOriginal.apply(console, arguments);
if (!console.reportErrorsAsExceptions) {
return;
}
if (inExceptionHandler) {
// The fundamental trick here is that are multiple entry point to logging errors:
// (see D19743075 for more background)
//
// 1. An uncaught exception being caught by the global handler
// 2. An error being logged throw console.error
//
// However, console.error is monkey patched multiple times: by this module, and by the
// DevTools setup that sends messages to Metro.
// The patching order cannot be relied upon.
//
// So, some scenarios that are handled by this flag:
//
// Logging an error:
// 1. console.error called from user code
// 2. (possibly) arrives _first_ at DevTool handler, send to Metro
// 3. Bubbles to here
// 4. goes into report Exception.
// 5. should not trigger console.error again, to avoid looping / logging twice
// 6. should still bubble up to original console
// (which might either be console.log, or the DevTools handler in case it patched _earlier_ and (2) didn't happen)
//
// Throwing an uncaught exception:
// 1. exception thrown
// 2. picked up by handleException
// 3. should be send to console.error (not console._errorOriginal, as DevTools might have patched _later_ and it needs to send it to Metro)
// 4. that _might_ bubble again to the `reactConsoleErrorHandle` defined here
// -> should not handle exception _again_, to avoid looping / showing twice (this code branch)
// 5. should still bubble up to original console (which might either be console.log, or the DevTools handler in case that one patched _earlier_)
return;
}
if (arguments[0] && arguments[0].stack) {
// reportException will console.error this with high enough fidelity.
reportException(
arguments[0],
/* isFatal */ false,
/*reportToConsole*/ false,
);
} else {
const stringifySafe = require('../Utilities/stringifySafe').default;
const str = Array.prototype.map
.call(arguments, value =>
typeof value === 'string' ? value : stringifySafe(value),
)
.join(' ');
if (str.slice(0, 9) === 'Warning: ') {
// React warnings use console.error so that a stack trace is shown, but
// we don't (currently) want these to show a redbox
// (Note: Logic duplicated in polyfills/console.js.)
return;
}
const error: ExtendedError = new SyntheticError(str);
error.name = 'console.error';
reportException(error, /* isFatal */ false, /*reportToConsole*/ false);
}
}
/**
* Shows a redbox with stacktrace for all console.error messages. Disable by
* setting `console.reportErrorsAsExceptions = false;` in your app.
*/
function installConsoleErrorReporter() {
// Enable reportErrorsAsExceptions
if (console._errorOriginal) {
return; // already installed
}
// Flow doesn't like it when you set arbitrary values on a global object
console._errorOriginal = console.error.bind(console);
console.error = reactConsoleErrorHandler;
if (console.reportErrorsAsExceptions === undefined) {
// Individual apps can disable this
// Flow doesn't like it when you set arbitrary values on a global object
console.reportErrorsAsExceptions = true;
}
}
module.exports = {
handleException,
installConsoleErrorReporter,
SyntheticError,
unstable_setExceptionDecorator,
};