@samitouri / QOS-React-2 / commits / 5022fdfd5f

Refactor Error Dialog Logging (#18487)

* Remove unnecessary CapturedError fields. componentName is not necessary and is misleading when the error is caused elsewhere in the stack. The stack is sufficient. The many error boundary fields are unnecessary because they can be inferred by the boundary itself. * Don't attempt to build a stack twice If it was possible, it would've been done in createCapturedValue. * Push the work needed by the works into the forks This avoids needing this in the npm published case.

Sebastian Markbåge committed Apr 3, 2020 at 19:01 UTC 5022fdfd5f9b3639c9c8bb2df31100586d1cfd01
7 files changed +111 -120
packages/react-reconciler/src/ReactCapturedValue.js
-10
@@ -17,16 +17,6 @@ export type CapturedValue<T> = {|
17 stack: string | null,
18 |};
19
20 -export type CapturedError = {|
21 - componentName: ?string,
22 - componentStack: string,
23 - error: mixed,
24 - errorBoundary: ?Object,
25 - errorBoundaryFound: boolean,
26 - errorBoundaryName: string | null,
27 - willRetry: boolean,
28 -|};
29 -
20 export function createCapturedValue<T>(
21 value: T,
22 source: Fiber,
packages/react-reconciler/src/ReactFiberCommitWork.js
-39
@@ -18,7 +18,6 @@ import type {
18 import type {Fiber} from './ReactFiber';
19 import type {FiberRoot} from './ReactFiberRoot';
20 import type {ExpirationTime} from './ReactFiberExpirationTime';
21 -import type {CapturedValue, CapturedError} from './ReactCapturedValue';
21 import type {SuspenseState} from './ReactFiberSuspenseComponent';
22 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
23 import type {Wakeable} from 'shared/ReactTypes';
@@ -75,7 +74,6 @@ import invariant from 'shared/invariant';
74
75 import {onCommitUnmount} from './ReactFiberDevToolsHook';
76 import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
78 -import {logCapturedError} from './ReactFiberErrorLogger';
77 import {resolveDefaultProps} from './ReactFiberLazyComponent';
78 import {
79 getCommitTime,
@@ -143,43 +141,6 @@ if (__DEV__) {
141
142 const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
143
146 -export function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) {
147 - const source = errorInfo.source;
148 - let stack = errorInfo.stack;
149 - if (stack === null && source !== null) {
150 - stack = getStackByFiberInDevAndProd(source);
151 - }
152 -
153 - const capturedError: CapturedError = {
154 - componentName: source !== null ? getComponentName(source.type) : null,
155 - componentStack: stack !== null ? stack : '',
156 - error: errorInfo.value,
157 - errorBoundary: null,
158 - errorBoundaryName: null,
159 - errorBoundaryFound: false,
160 - willRetry: false,
161 - };
162 -
163 - if (boundary !== null && boundary.tag === ClassComponent) {
164 - capturedError.errorBoundary = boundary.stateNode;
165 - capturedError.errorBoundaryName = getComponentName(boundary.type);
166 - capturedError.errorBoundaryFound = true;
167 - capturedError.willRetry = true;
168 - }
169 -
170 - try {
171 - logCapturedError(capturedError);
172 - } catch (e) {
173 - // This method must not throw, or React internal state will get messed up.
174 - // If console.error is overridden, or logCapturedError() shows a dialog that throws,
175 - // we want to report this error outside of the normal stack as a last resort.
176 - // https://github.com/facebook/react/issues/13188
177 - setTimeout(() => {
178 - throw e;
179 - });
180 - }
181 -}
182 -
144 const callComponentWillUnmountWithTimer = function(current, instance) {
145 instance.props = current.memoizedProps;
146 instance.state = current.memoizedState;
packages/react-reconciler/src/ReactFiberErrorDialog.js
+7 -2
@@ -7,11 +7,16 @@
7 * @flow
8 */
9
10 -import type {CapturedError} from './ReactCapturedValue';
10 +import type {Fiber} from './ReactFiber';
11 +import type {CapturedValue} from './ReactCapturedValue';
12
13 // This module is forked in different environments.
14 // By default, return `true` to log errors to the console.
15 // Forks can return `false` if this isn't desirable.
15 -export function showErrorDialog(capturedError: CapturedError): boolean {
16 +
17 +export function showErrorDialog(
18 + boundary: Fiber,
19 + errorInfo: CapturedValue<mixed>,
20 +): boolean {
21 return true;
22 }
packages/react-reconciler/src/ReactFiberErrorLogger.js
+67 -61
@@ -7,82 +7,88 @@
7 * @flow
8 */
9
10 -import type {CapturedError} from './ReactCapturedValue';
10 +import type {Fiber} from './ReactFiber';
11 +import type {CapturedValue} from './ReactCapturedValue';
12
13 import {showErrorDialog} from './ReactFiberErrorDialog';
14 +import {ClassComponent} from './ReactWorkTags';
15 +import getComponentName from 'shared/getComponentName';
16
14 -export function logCapturedError(capturedError: CapturedError): void {
15 - const logError = showErrorDialog(capturedError);
17 +export function logCapturedError(
18 + boundary: Fiber,
19 + errorInfo: CapturedValue<mixed>,
20 +): void {
21 + try {
22 + const logError = showErrorDialog(boundary, errorInfo);
23
17 - // Allow injected showErrorDialog() to prevent default console.error logging.
18 - // This enables renderers like ReactNative to better manage redbox behavior.
19 - if (logError === false) {
20 - return;
21 - }
22 -
23 - const error = (capturedError.error: any);
24 - if (__DEV__) {
25 - const {
26 - componentName,
27 - componentStack,
28 - errorBoundaryName,
29 - errorBoundaryFound,
30 - willRetry,
31 - } = capturedError;
24 + // Allow injected showErrorDialog() to prevent default console.error logging.
25 + // This enables renderers like ReactNative to better manage redbox behavior.
26 + if (logError === false) {
27 + return;
28 + }
29
33 - // Browsers support silencing uncaught errors by calling
34 - // `preventDefault()` in window `error` handler.
35 - // We record this information as an expando on the error.
36 - if (error != null && error._suppressLogging) {
37 - if (errorBoundaryFound && willRetry) {
38 - // The error is recoverable and was silenced.
39 - // Ignore it and don't print the stack addendum.
40 - // This is handy for testing error boundaries without noise.
41 - return;
30 + const error = (errorInfo.value: any);
31 + if (__DEV__) {
32 + const source = errorInfo.source;
33 + const stack = errorInfo.stack;
34 + const componentStack = stack !== null ? stack : '';
35 + // Browsers support silencing uncaught errors by calling
36 + // `preventDefault()` in window `error` handler.
37 + // We record this information as an expando on the error.
38 + if (error != null && error._suppressLogging) {
39 + if (boundary.tag === ClassComponent) {
40 + // The error is recoverable and was silenced.
41 + // Ignore it and don't print the stack addendum.
42 + // This is handy for testing error boundaries without noise.
43 + return;
44 + }
45 + // The error is fatal. Since the silencing might have
46 + // been accidental, we'll surface it anyway.
47 + // However, the browser would have silenced the original error
48 + // so we'll print it first, and then print the stack addendum.
49 + console['error'](error); // Don't transform to our wrapper
50 + // For a more detailed description of this block, see:
51 + // https://github.com/facebook/react/pull/13384
52 }
43 - // The error is fatal. Since the silencing might have
44 - // been accidental, we'll surface it anyway.
45 - // However, the browser would have silenced the original error
46 - // so we'll print it first, and then print the stack addendum.
47 - console['error'](error); // Don't transform to our wrapper
48 - // For a more detailed description of this block, see:
49 - // https://github.com/facebook/react/pull/13384
50 - }
53
52 - const componentNameMessage = componentName
53 - ? `The above error occurred in the <${componentName}> component:`
54 - : 'The above error occurred in one of your React components:';
54 + const componentName = source ? getComponentName(source.type) : null;
55 + const componentNameMessage = componentName
56 + ? `The above error occurred in the <${componentName}> component:`
57 + : 'The above error occurred in one of your React components:';
58
56 - let errorBoundaryMessage;
57 - // errorBoundaryFound check is sufficient; errorBoundaryName check is to satisfy Flow.
58 - if (errorBoundaryFound && errorBoundaryName) {
59 - if (willRetry) {
59 + let errorBoundaryMessage;
60 + const errorBoundaryName = getComponentName(boundary.type);
61 + if (errorBoundaryName) {
62 errorBoundaryMessage =
63 `React will try to recreate this component tree from scratch ` +
64 `using the error boundary you provided, ${errorBoundaryName}.`;
65 } else {
66 errorBoundaryMessage =
65 - `This error was initially handled by the error boundary ${errorBoundaryName}.\n` +
66 - `Recreating the tree from scratch failed so React will unmount the tree.`;
67 + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
68 + 'Visit https://fb.me/react-error-boundaries to learn more about error boundaries.';
69 }
70 + const combinedMessage =
71 + `${componentNameMessage}\n${componentStack}\n\n` +
72 + `${errorBoundaryMessage}`;
73 +
74 + // In development, we provide our own message with just the component stack.
75 + // We don't include the original error message and JS stack because the browser
76 + // has already printed it. Even if the application swallows the error, it is still
77 + // displayed by the browser thanks to the DEV-only fake event trick in ReactErrorUtils.
78 + console['error'](combinedMessage); // Don't transform to our wrapper
79 } else {
69 - errorBoundaryMessage =
70 - 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
71 - 'Visit https://fb.me/react-error-boundaries to learn more about error boundaries.';
80 + // In production, we print the error directly.
81 + // This will include the message, the JS stack, and anything the browser wants to show.
82 + // We pass the error object instead of custom message so that the browser displays the error natively.
83 + console['error'](error); // Don't transform to our wrapper
84 }
73 - const combinedMessage =
74 - `${componentNameMessage}${componentStack}\n\n` +
75 - `${errorBoundaryMessage}`;
76 -
77 - // In development, we provide our own message with just the component stack.
78 - // We don't include the original error message and JS stack because the browser
79 - // has already printed it. Even if the application swallows the error, it is still
80 - // displayed by the browser thanks to the DEV-only fake event trick in ReactErrorUtils.
81 - console['error'](combinedMessage); // Don't transform to our wrapper
82 - } else {
83 - // In production, we print the error directly.
84 - // This will include the message, the JS stack, and anything the browser wants to show.
85 - // We pass the error object instead of custom message so that the browser displays the error natively.
86 - console['error'](error); // Don't transform to our wrapper
85 + } catch (e) {
86 + // This method must not throw, or React internal state will get messed up.
87 + // If console.error is overridden, or logCapturedError() shows a dialog that throws,
88 + // we want to report this error outside of the normal stack as a last resort.
89 + // https://github.com/facebook/react/issues/13188
90 + setTimeout(() => {
91 + throw e;
92 + });
93 }
94 }
packages/react-reconciler/src/ReactFiberThrow.js
+4 -4
@@ -40,7 +40,6 @@ import {
40 ForceUpdate,
41 enqueueUpdate,
42 } from './ReactUpdateQueue';
43 -import {logError} from './ReactFiberCommitWork';
43 import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
44 import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading';
45 import {
@@ -55,6 +54,7 @@ import {
54 isAlreadyFailedLegacyErrorBoundary,
55 pingSuspendedRoot,
56 } from './ReactFiberWorkLoop';
57 +import {logCapturedError} from './ReactFiberErrorLogger';
58
59 import {Sync} from './ReactFiberExpirationTime';
60
@@ -74,7 +74,7 @@ function createRootErrorUpdate(
74 const error = errorInfo.value;
75 update.callback = () => {
76 onUncaughtError(error);
77 - logError(fiber, errorInfo);
77 + logCapturedError(fiber, errorInfo);
78 };
79 return update;
80 }
@@ -90,7 +90,7 @@ function createClassErrorUpdate(
90 if (typeof getDerivedStateFromError === 'function') {
91 const error = errorInfo.value;
92 update.payload = () => {
93 - logError(fiber, errorInfo);
93 + logCapturedError(fiber, errorInfo);
94 return getDerivedStateFromError(error);
95 };
96 }
@@ -110,7 +110,7 @@ function createClassErrorUpdate(
110 markLegacyErrorBoundaryAsFailed(this);
111
112 // Only log here if componentDidCatch is the only error boundary method defined
113 - logError(fiber, errorInfo);
113 + logCapturedError(fiber, errorInfo);
114 }
115 const error = errorInfo.value;
116 const stack = errorInfo.stack;
packages/react-reconciler/src/forks/ReactFiberErrorDialog.native.js
+17 -2
@@ -7,10 +7,14 @@
7 * @flow
8 */
9
10 -import type {CapturedError} from '../ReactCapturedValue';
10 +import type {Fiber} from '../ReactFiber';
11 +import type {CapturedValue} from '../ReactCapturedValue';
12 +
13 +import {ClassComponent} from '../ReactWorkTags';
14
15 // Module provided by RN:
16 import {ReactFiberErrorDialog as RNImpl} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
17 +
18 import invariant from 'shared/invariant';
19
20 invariant(
@@ -18,6 +22,17 @@ invariant(
22 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
23 );
24
21 -export function showErrorDialog(capturedError: CapturedError): boolean {
25 +export function showErrorDialog(
26 + boundary: Fiber,
27 + errorInfo: CapturedValue<mixed>,
28 +): boolean {
29 + const capturedError = {
30 + componentStack: errorInfo.stack !== null ? errorInfo.stack : '',
31 + error: errorInfo.value,
32 + errorBoundary:
33 + boundary !== null && boundary.tag === ClassComponent
34 + ? boundary.stateNode
35 + : null,
36 + };
37 return RNImpl.showErrorDialog(capturedError);
38 }
packages/react-reconciler/src/forks/ReactFiberErrorDialog.www.js
+16 -2
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import type {CapturedError} from '../ReactCapturedValue';
10 +import type {Fiber} from '../ReactFiber';
11 +import type {CapturedValue} from '../ReactCapturedValue';
12 +
13 +import {ClassComponent} from '../ReactWorkTags';
14
15 import invariant from 'shared/invariant';
16
@@ -18,6 +21,17 @@ invariant(
21 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
22 );
23
21 -export function showErrorDialog(capturedError: CapturedError): boolean {
24 +export function showErrorDialog(
25 + boundary: Fiber,
26 + errorInfo: CapturedValue<mixed>,
27 +): boolean {
28 + const capturedError = {
29 + componentStack: errorInfo.stack !== null ? errorInfo.stack : '',
30 + error: errorInfo.value,
31 + errorBoundary:
32 + boundary !== null && boundary.tag === ClassComponent
33 + ? boundary.stateNode
34 + : null,
35 + };
36 return ReactFiberErrorDialogWWW.showErrorDialog(capturedError);
37 }