Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Libraries/Core/ReactFiberErrorDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* 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
*/

export type CapturedError = {
+componentName: ?string,
+componentStack: string,
+error: mixed,
+errorBoundary: ?{},
+errorBoundaryFound: boolean,
+errorBoundaryName: string | null,
+willRetry: boolean,
};

import {handleException} from './ExceptionsManager';

/**
* Intercept lifecycle errors and ensure they are shown with the correct stack
* trace within the native redbox component.
*/
export function showErrorDialog(capturedError: CapturedError): boolean {
const {componentStack, error} = capturedError;

let errorToHandle: Error;

// Typically Errors are thrown but eg strings or null can be thrown as well.
if (error instanceof Error) {
const {message, name} = error;

const summary = message ? `${name}: ${message}` : name;

errorToHandle = error;

try {
errorToHandle.message = `${summary}\n\nThis error is located at:${componentStack}`;
} catch (e) {}
} else if (typeof error === 'string') {
errorToHandle = new Error(
`${error}\n\nThis error is located at:${componentStack}`,
);
} else {
errorToHandle = new Error(`Unspecified error at:${componentStack}`);
}

handleException(errorToHandle, false);

// Return false here to prevent ReactFiberErrorLogger default behavior of
// logging error details to console.error. Calls to console.error are
// automatically routed to the native redbox controller, which we've already
// done above by calling ExceptionsManager.
return false;
}
109 changes: 109 additions & 0 deletions Libraries/Core/__tests__/ReactFiberErrorDialog-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* 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
* @emails oncall+react_native
*/
'use strict';

const capturedErrorDefaults = {
componentName: 'A',
componentStack: '\n in A\n in B\n in C',
errorBoundary: null,
errorBoundaryFound: false,
errorBoundaryName: null,
willRetry: false,
};

describe('ReactFiberErrorDialog', () => {
let ReactFiberErrorDialog, ExceptionsManager;
beforeEach(() => {
jest.resetModules();
jest.mock('../ExceptionsManager', () => {
return {
handleException: jest.fn(),
};
});
ReactFiberErrorDialog = require('../ReactFiberErrorDialog');
ExceptionsManager = require('../ExceptionsManager');
});

describe('showErrorDialog', () => {
test('forwards error instance to handleException', () => {
const error = new ReferenceError('Some error happened');
error.someCustomProp = 42;
// Copy all the data we care about before any possible mutation.
const {name, stack, message, someCustomProp} = error;

const logToConsole = ReactFiberErrorDialog.showErrorDialog({
...capturedErrorDefaults,
error,
});

expect(ExceptionsManager.handleException.mock.calls.length).toBe(1);
const errorArg = ExceptionsManager.handleException.mock.calls[0][0];
const isFatalArg = ExceptionsManager.handleException.mock.calls[0][1];
// We intentionally don't test whether errorArg === error, because this
// implementation detail might change. Instead, we test that they are
// functionally equivalent.
expect(errorArg).toBeInstanceOf(ReferenceError);
expect(errorArg).toHaveProperty('name', name);
expect(errorArg).toHaveProperty('stack', stack);
expect(errorArg).toHaveProperty('someCustomProp', someCustomProp);
expect(errorArg).toHaveProperty(
'message',
'ReferenceError: ' +
message +
'\n\n' +
'This error is located at:' +
capturedErrorDefaults.componentStack,
);
expect(isFatalArg).toBe(false);
expect(logToConsole).toBe(false);
});

test('wraps string in an Error and sends to handleException', () => {
const message = 'Some error happened';

const logToConsole = ReactFiberErrorDialog.showErrorDialog({
...capturedErrorDefaults,
error: message,
});

expect(ExceptionsManager.handleException.mock.calls.length).toBe(1);
const errorArg = ExceptionsManager.handleException.mock.calls[0][0];
const isFatalArg = ExceptionsManager.handleException.mock.calls[0][1];
expect(errorArg).toBeInstanceOf(Error);
expect(errorArg).toHaveProperty(
'message',
message +
'\n\n' +
'This error is located at:' +
capturedErrorDefaults.componentStack,
);
expect(isFatalArg).toBe(false);
expect(logToConsole).toBe(false);
});

test('reports "Unspecified error" if error is null', () => {
const logToConsole = ReactFiberErrorDialog.showErrorDialog({
...capturedErrorDefaults,
error: null,
});

expect(ExceptionsManager.handleException.mock.calls.length).toBe(1);
const errorArg = ExceptionsManager.handleException.mock.calls[0][0];
const isFatalArg = ExceptionsManager.handleException.mock.calls[0][1];
expect(errorArg).toBeInstanceOf(Error);
expect(errorArg).toHaveProperty(
'message',
'Unspecified error at:' + capturedErrorDefaults.componentStack,
);
expect(isFatalArg).toBe(false);
expect(logToConsole).toBe(false);
});
});
});
3 changes: 3 additions & 0 deletions Libraries/ReactPrivate/ReactNativePrivateInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ module.exports = {
get flattenStyle() {
return require('../StyleSheet/flattenStyle');
},
get ReactFiberErrorDialog() {
return require('../Core/ReactFiberErrorDialog');
},
};