@samitouri / QOS-React / commits / 224487a0e2

Show more meanignful stack trace for ReactNative errors (#9291)

* Show more meanignful stack trace for ReactNative errors Clicking on the stack should jump to where the error actually occurred rather than to where it's logged in a redbox component. * Made showDialog() return type stricter. Added mock and Flow types for ExceptionManager * Prettier * Handle null/string thrown errors * Removed unused reference to emptyFunction

Brian Vaughn committed Mar 30, 2017 at 15:15 UTC 224487a0e27f6e8e64ef9504ed93d8d9bfefe249
5 files changed +102 -6
flow/react-native-host-hooks.js
+6
@@ -26,6 +26,12 @@ declare module 'TextInputState' {
26 declare function blurTextInput(object : any) : void;
27 declare function focusTextInput(object : any) : void;
28 }
29 +declare module 'ExceptionsManager' {
30 + declare function handleException(
31 + error: Error,
32 + isFatal: boolean,
33 + ) : void;
34 +}
35 declare module 'UIManager' {
36 declare var customBubblingEventTypes : Object;
37 declare var customDirectEventTypes : Object;
src/__mocks__/ExceptionsManager.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright 2013-2015, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + */
9 +
10 +'use strict';
11 +
12 +module.exports = {
13 + handleException: jest.fn(),
14 +};
src/renderers/native/ReactNativeFiber.js
+8
@@ -12,10 +12,12 @@
12
13 'use strict';
14
15 +const ReactFiberErrorLogger = require('ReactFiberErrorLogger');
16 const ReactFiberReconciler = require('ReactFiberReconciler');
17 const ReactGenericBatching = require('ReactGenericBatching');
18 const ReactNativeAttributePayload = require('ReactNativeAttributePayload');
19 const ReactNativeComponentTree = require('ReactNativeComponentTree');
20 +const ReactNativeFiberErrorDialog = require('ReactNativeFiberErrorDialog');
21 const ReactNativeFiberHostComponent = require('ReactNativeFiberHostComponent');
22 const ReactNativeInjection = require('ReactNativeInjection');
23 const ReactNativeTagHandles = require('ReactNativeTagHandles');
@@ -377,6 +379,12 @@ findNodeHandle.injection.injectFindNode((fiber: Fiber) =>
379 NativeRenderer.findHostInstance(fiber));
380 findNodeHandle.injection.injectFindRootNodeID(instance => instance);
381
382 +// Intercept lifecycle errors and ensure they are shown with the correct stack
383 +// trace within the native redbox component.
384 +ReactFiberErrorLogger.injection.injectDialog(
385 + ReactNativeFiberErrorDialog.showDialog,
386 +);
387 +
388 const ReactNative = {
389 // External users of findNodeHandle() expect the host tag number return type.
390 // The injected findNodeHandle() strategy returns the instance wrapper though.
src/renderers/native/ReactNativeFiberErrorDialog.js new
+57
@@ -0,0 +1,57 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactNativeFiberErrorDialog
10 + * @flow
11 + */
12 +
13 +'use strict';
14 +
15 +const ExceptionsManager = require('ExceptionsManager');
16 +
17 +import type {CapturedError} from 'ReactFiberScheduler';
18 +
19 +/**
20 + * Intercept lifecycle errors and ensure they are shown with the correct stack
21 + * trace within the native redbox component.
22 + */
23 +function ReactNativeFiberErrorDialog(capturedError: CapturedError): boolean {
24 + const {componentStack, error} = capturedError;
25 +
26 + let errorMessage: string;
27 + let errorStack: string;
28 + let errorType: Class<Error>;
29 +
30 + // Typically Errors are thrown but eg strings or null can be thrown as well.
31 + if (error && typeof error === 'object') {
32 + const {message, name} = error;
33 +
34 + const summary = message ? `${name}: ${message}` : name;
35 +
36 + errorMessage = `${summary}\n\nThis error is located at:${componentStack}`;
37 + errorStack = error.stack;
38 + errorType = error.constructor;
39 + } else {
40 + errorMessage = `Unspecified error at:${componentStack}`;
41 + errorStack = '';
42 + errorType = Error;
43 + }
44 +
45 + const newError = new errorType(errorMessage);
46 + newError.stack = errorStack;
47 +
48 + ExceptionsManager.handleException(newError, false);
49 +
50 + // Return false here to prevent ReactFiberErrorLogger default behavior of
51 + // logging error details to console.error. Calls to console.error are
52 + // automatically routed to the native redbox controller, which we've already
53 + // done above by calling ExceptionsManager.
54 + return false;
55 +}
56 +
57 +module.exports.showDialog = ReactNativeFiberErrorDialog;
src/renderers/shared/fiber/ReactFiberErrorLogger.js
+17 -6
@@ -12,14 +12,23 @@
12
13 'use strict';
14
15 -const emptyFunction = require('fbjs/lib/emptyFunction');
15 const invariant = require('fbjs/lib/invariant');
16
17 import type {CapturedError} from 'ReactFiberScheduler';
18
20 -let showDialog = emptyFunction;
19 +const defaultShowDialog = () => true;
20 +
21 +let showDialog = defaultShowDialog;
22
23 function logCapturedError(capturedError: CapturedError): void {
24 + const logError = showDialog(capturedError);
25 +
26 + // Allow injected showDialog() to prevent default console.error logging.
27 + // This enables renderers like ReactNative to better manage redbox behavior.
28 + if (logError === false) {
29 + return;
30 + }
31 +
32 if (__DEV__) {
33 const {
34 componentName,
@@ -85,14 +94,16 @@ function logCapturedError(capturedError: CapturedError): void {
94 `React caught an error thrown by one of your components.\n\n${error.stack}`,
95 );
96 }
88 -
89 - showDialog(capturedError);
97 }
98
99 exports.injection = {
93 - injectDialog(fn: (e: CapturedError) => void) {
100 + /**
101 + * Display custom dialog for lifecycle errors.
102 + * Return false to prevent default behavior of logging to console.error.
103 + */
104 + injectDialog(fn: (e: CapturedError) => boolean) {
105 invariant(
95 - showDialog === emptyFunction,
106 + showDialog === defaultShowDialog,
107 'The custom dialog was already injected.',
108 );
109 invariant(