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(