Skip to content

Commit

Permalink
Show more meaningful error stack in ReactNative redbox
Browse files Browse the repository at this point in the history
Reviewed By: yungsters

Differential Revision: D4797372

fbshipit-source-id: 069c013bcc3d58dd38a25979f4a04aed5fc1dde6
  • Loading branch information
Brian Vaughn authored and facebook-github-bot committed Mar 31, 2017
1 parent 7db16a4 commit d4aa42a
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 6 deletions.
9 changes: 9 additions & 0 deletions Libraries/Renderer/src/renderers/native/ReactNativeFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@

'use strict';

const ReactFiberErrorLogger = require('ReactFiberErrorLogger');
const ReactFiberReconciler = require('ReactFiberReconciler');
const ReactGenericBatching = require('ReactGenericBatching');
const ReactNativeAttributePayload = require('ReactNativeAttributePayload');
const ReactNativeComponentTree = require('ReactNativeComponentTree');
const ReactNativeFiberErrorDialog = require('ReactNativeFiberErrorDialog');
const ReactNativeFiberHostComponent = require('ReactNativeFiberHostComponent');
const ReactNativeInjection = require('ReactNativeInjection');
const ReactNativeTagHandles = require('ReactNativeTagHandles');
Expand Down Expand Up @@ -377,6 +379,13 @@ findNodeHandle.injection.injectFindNode((fiber: Fiber) =>
NativeRenderer.findHostInstance(fiber));
findNodeHandle.injection.injectFindRootNodeID(instance => instance);


// Intercept lifecycle errors and ensure they are shown with the correct stack
// trace within the native redbox component.
ReactFiberErrorLogger.injection.injectDialog(
ReactNativeFiberErrorDialog.showDialog,
);

const ReactNative = {
// External users of findNodeHandle() expect the host tag number return type.
// The injected findNodeHandle() strategy returns the instance wrapper though.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeFiberErrorDialog
* @flow
*/

'use strict';

const ExceptionsManager = require('ExceptionsManager');

import type {CapturedError} from 'ReactFiberScheduler';

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

let errorMessage: string;
let errorStack: string;
let errorType: Class<Error>;

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

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

errorMessage = `${summary}\n\nThis error is located at:${componentStack}`;
errorStack = error.stack;
errorType = error.constructor;
} else {
errorMessage = `Unspecified error at:${componentStack}`;
errorStack = '';
errorType = Error;
}

const newError = new errorType(errorMessage);
newError.stack = errorStack;

ExceptionsManager.handleException(newError, 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;
}

module.exports.showDialog = ReactNativeFiberErrorDialog;
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,23 @@

'use strict';

const emptyFunction = require('fbjs/lib/emptyFunction');
const invariant = require('fbjs/lib/invariant');

import type {CapturedError} from 'ReactFiberScheduler';

let showDialog = emptyFunction;
const defaultShowDialog = () => true;

let showDialog = defaultShowDialog;

function logCapturedError(capturedError: CapturedError): void {
const logError = showDialog(capturedError);

// Allow injected showDialog() to prevent default console.error logging.
// This enables renderers like ReactNative to better manage redbox behavior.
if (logError === false) {
return;
}

if (__DEV__) {
const {
componentName,
Expand Down Expand Up @@ -85,14 +94,16 @@ function logCapturedError(capturedError: CapturedError): void {
`React caught an error thrown by one of your components.\n\n${error.stack}`,
);
}

showDialog(capturedError);
}

exports.injection = {
injectDialog(fn: (e: CapturedError) => void) {
/**
* Display custom dialog for lifecycle errors.
* Return false to prevent default behavior of logging to console.error.
*/
injectDialog(fn: (e: CapturedError) => boolean) {
invariant(
showDialog === emptyFunction,
showDialog === defaultShowDialog,
'The custom dialog was already injected.',
);
invariant(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
return null;
}
}

// Without this explicit null return Flow complains of invalid return type
return null;
}

function performUnitOfWork(workInProgress: Fiber): Fiber | null {
Expand Down

0 comments on commit d4aa42a

Please sign in to comment.