Use static injection for ReactErrorUtils (#11725)
* Use `this` inside invokeGuardedCallback It's slightly odd but that's exactly how our www fork works. Might as well do it in the open source version to make it clear we rely on context here. * Move invokeGuardedCallback into a separate file This lets us introduce forks for it. * Add a www fork for invokeGuardedCallback * Fix Flow
Dan Abramov committed
Nov 30, 2017 at 19:10 UTC
46b3c3e4ae0d52565f7ed2344036a22016781ca0
6 files changed
+245
-199
packages/react-dom/src/client/ReactDOMFB.js
-2
@@ -9,7 +9,6 @@
9
10
import * as ReactFiberTreeReflection from 'react-reconciler/reflection';
11
import * as ReactInstanceMap from 'shared/ReactInstanceMap';
12
-import ReactErrorUtils from 'shared/ReactErrorUtils';
12
import {addUserTimingListener} from 'shared/ReactFeatureFlags';
13
14
import ReactDOM from './ReactDOM';
@@ -22,7 +21,6 @@ Object.assign(
21
{
22
// These are real internal dependencies that are trickier to remove:
23
ReactBrowserEventEmitter,
25
- ReactErrorUtils,
24
ReactFiberTreeReflection,
25
ReactDOMComponentTree,
26
ReactInstanceMap,
packages/shared/ReactErrorUtils.js
+1
-172
@@ -8,6 +8,7 @@
8
*/
9
10
import invariant from 'fbjs/lib/invariant';
11
+import invokeGuardedCallback from './invokeGuardedCallback';
12
13
const ReactErrorUtils = {
14
// Used by Fiber to simulate a try-catch.
@@ -18,16 +19,6 @@ const ReactErrorUtils = {
19
_rethrowError: (null: mixed),
20
_hasRethrowError: (false: boolean),
21
21
- injection: {
22
- injectErrorUtils(injectedErrorUtils: Object) {
23
- invariant(
24
- typeof injectedErrorUtils.invokeGuardedCallback === 'function',
25
- 'Injected invokeGuardedCallback() must be a function.',
26
- );
27
- invokeGuardedCallback = injectedErrorUtils.invokeGuardedCallback;
28
- },
29
- },
30
-
22
/**
23
* Call a function while guarding against errors that happens within it.
24
* Returns an error if it throws, otherwise null.
@@ -114,168 +105,6 @@ const ReactErrorUtils = {
105
},
106
};
107
117
-let invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) {
118
- ReactErrorUtils._hasCaughtError = false;
119
- ReactErrorUtils._caughtError = null;
120
- const funcArgs = Array.prototype.slice.call(arguments, 3);
121
- try {
122
- func.apply(context, funcArgs);
123
- } catch (error) {
124
- ReactErrorUtils._caughtError = error;
125
- ReactErrorUtils._hasCaughtError = true;
126
- }
127
-};
128
-
129
-if (__DEV__) {
130
- // In DEV mode, we swap out invokeGuardedCallback for a special version
131
- // that plays more nicely with the browser's DevTools. The idea is to preserve
132
- // "Pause on exceptions" behavior. Because React wraps all user-provided
133
- // functions in invokeGuardedCallback, and the production version of
134
- // invokeGuardedCallback uses a try-catch, all user exceptions are treated
135
- // like caught exceptions, and the DevTools won't pause unless the developer
136
- // takes the extra step of enabling pause on caught exceptions. This is
137
- // untintuitive, though, because even though React has caught the error, from
138
- // the developer's perspective, the error is uncaught.
139
- //
140
- // To preserve the expected "Pause on exceptions" behavior, we don't use a
141
- // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
142
- // DOM node, and call the user-provided callback from inside an event handler
143
- // for that fake event. If the callback throws, the error is "captured" using
144
- // a global event handler. But because the error happens in a different
145
- // event loop context, it does not interrupt the normal program flow.
146
- // Effectively, this gives us try-catch behavior without actually using
147
- // try-catch. Neat!
148
-
149
- // Check that the browser supports the APIs we need to implement our special
150
- // DEV version of invokeGuardedCallback
151
- if (
152
- typeof window !== 'undefined' &&
153
- typeof window.dispatchEvent === 'function' &&
154
- typeof document !== 'undefined' &&
155
- typeof document.createEvent === 'function'
156
- ) {
157
- const fakeNode = document.createElement('react');
158
-
159
- const invokeGuardedCallbackDev = function(
160
- name,
161
- func,
162
- context,
163
- a,
164
- b,
165
- c,
166
- d,
167
- e,
168
- f,
169
- ) {
170
- // If document doesn't exist we know for sure we will crash in this method
171
- // when we call document.createEvent(). However this can cause confusing
172
- // errors: https://github.com/facebookincubator/create-react-app/issues/3482
173
- // So we preemptively throw with a better message instead.
174
- invariant(
175
- typeof document !== 'undefined',
176
- 'The `document` global was defined when React was initialized, but is not ' +
177
- 'defined anymore. This can happen in a test environment if a component ' +
178
- 'schedules an update from an asynchronous callback, but the test has already ' +
179
- 'finished running. To solve this, you can either unmount the component at ' +
180
- 'the end of your test (and ensure that any asynchronous operations get ' +
181
- 'canceled in `componentWillUnmount`), or you can change the test itself ' +
182
- 'to be asynchronous.',
183
- );
184
- const evt = document.createEvent('Event');
185
-
186
- // Keeps track of whether the user-provided callback threw an error. We
187
- // set this to true at the beginning, then set it to false right after
188
- // calling the function. If the function errors, `didError` will never be
189
- // set to false. This strategy works even if the browser is flaky and
190
- // fails to call our global error handler, because it doesn't rely on
191
- // the error event at all.
192
- let didError = true;
193
-
194
- // Create an event handler for our fake event. We will synchronously
195
- // dispatch our fake event using `dispatchEvent`. Inside the handler, we
196
- // call the user-provided callback.
197
- const funcArgs = Array.prototype.slice.call(arguments, 3);
198
- function callCallback() {
199
- // We immediately remove the callback from event listeners so that
200
- // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
201
- // nested call would trigger the fake event handlers of any call higher
202
- // in the stack.
203
- fakeNode.removeEventListener(evtType, callCallback, false);
204
- func.apply(context, funcArgs);
205
- didError = false;
206
- }
207
-
208
- // Create a global error event handler. We use this to capture the value
209
- // that was thrown. It's possible that this error handler will fire more
210
- // than once; for example, if non-React code also calls `dispatchEvent`
211
- // and a handler for that event throws. We should be resilient to most of
212
- // those cases. Even if our error event handler fires more than once, the
213
- // last error event is always used. If the callback actually does error,
214
- // we know that the last error event is the correct one, because it's not
215
- // possible for anything else to have happened in between our callback
216
- // erroring and the code that follows the `dispatchEvent` call below. If
217
- // the callback doesn't error, but the error event was fired, we know to
218
- // ignore it because `didError` will be false, as described above.
219
- let error;
220
- // Use this to track whether the error event is ever called.
221
- let didSetError = false;
222
- let isCrossOriginError = false;
223
-
224
- function onError(event) {
225
- error = event.error;
226
- didSetError = true;
227
- if (error === null && event.colno === 0 && event.lineno === 0) {
228
- isCrossOriginError = true;
229
- }
230
- }
231
-
232
- // Create a fake event type.
233
- const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
234
-
235
- // Attach our event handlers
236
- window.addEventListener('error', onError);
237
- fakeNode.addEventListener(evtType, callCallback, false);
238
-
239
- // Synchronously dispatch our fake event. If the user-provided function
240
- // errors, it will trigger our global error handler.
241
- evt.initEvent(evtType, false, false);
242
- fakeNode.dispatchEvent(evt);
243
-
244
- if (didError) {
245
- if (!didSetError) {
246
- // The callback errored, but the error event never fired.
247
- error = new Error(
248
- 'An error was thrown inside one of your components, but React ' +
249
- "doesn't know what it was. This is likely due to browser " +
250
- 'flakiness. React does its best to preserve the "Pause on ' +
251
- 'exceptions" behavior of the DevTools, which requires some ' +
252
- "DEV-mode only tricks. It's possible that these don't work in " +
253
- 'your browser. Try triggering the error in production mode, ' +
254
- 'or switching to a modern browser. If you suspect that this is ' +
255
- 'actually an issue with React, please file an issue.',
256
- );
257
- } else if (isCrossOriginError) {
258
- error = new Error(
259
- "A cross-origin error was thrown. React doesn't have access to " +
260
- 'the actual error object in development. ' +
261
- 'See https://fb.me/react-crossorigin-error for more information.',
262
- );
263
- }
264
- ReactErrorUtils._hasCaughtError = true;
265
- ReactErrorUtils._caughtError = error;
266
- } else {
267
- ReactErrorUtils._hasCaughtError = false;
268
- ReactErrorUtils._caughtError = null;
269
- }
270
-
271
- // Remove our event listeners
272
- window.removeEventListener('error', onError);
273
- };
274
-
275
- invokeGuardedCallback = invokeGuardedCallbackDev;
276
- }
277
-}
278
-
108
let rethrowCaughtError = function() {
109
if (ReactErrorUtils._hasRethrowError) {
110
const error = ReactErrorUtils._rethrowError;
packages/shared/__tests__/ReactErrorUtils-test.internal.js
+31
-24
@@ -176,30 +176,37 @@ describe('ReactErrorUtils', () => {
176
177
it(`can be shimmed`, () => {
178
const ops = [];
179
- // Override the original invokeGuardedCallback
180
- ReactErrorUtils.injection.injectErrorUtils({
181
- invokeGuardedCallback(name, func, context, a) {
182
- ops.push(a);
183
- try {
184
- func.call(context, a);
185
- } catch (error) {
186
- this._hasCaughtError = true;
187
- this._caughtError = error;
188
- }
189
- },
190
- });
191
-
192
- var err = new Error('foo');
193
- var callback = function() {
194
- throw err;
195
- };
196
- ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
197
- 'foo',
198
- callback,
199
- null,
200
- 'somearg',
179
+ jest.resetModules();
180
+ jest.mock(
181
+ 'shared/invokeGuardedCallback',
182
+ () =>
183
+ function invokeGuardedCallback(name, func, context, a) {
184
+ ops.push(a);
185
+ try {
186
+ func.call(context, a);
187
+ } catch (error) {
188
+ this._hasCaughtError = true;
189
+ this._caughtError = error;
190
+ }
191
+ },
192
);
202
- expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
203
- expect(ops).toEqual(['somearg']);
193
+ ReactErrorUtils = require('shared/ReactErrorUtils').default;
194
+
195
+ try {
196
+ var err = new Error('foo');
197
+ var callback = function() {
198
+ throw err;
199
+ };
200
+ ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
201
+ 'foo',
202
+ callback,
203
+ null,
204
+ 'somearg',
205
+ );
206
+ expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
207
+ expect(ops).toEqual(['somearg']);
208
+ } finally {
209
+ jest.unmock('shared/invokeGuardedCallback');
210
+ }
211
});
212
});
packages/shared/forks/invokeGuardedCallback.www.js
new
+17
@@ -0,0 +1,17 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import invariant from 'fbjs/lib/invariant';
9
+
10
+const invokeGuardedCallback = require('ReactFbErrorUtils')
11
+ .invokeGuardedCallback;
12
+invariant(
13
+ typeof invokeGuardedCallback === 'function',
14
+ 'Expected ReactFbErrorUtils.invokeGuardedCallback to be a function.',
15
+);
16
+
17
+export default invokeGuardedCallback;
packages/shared/invokeGuardedCallback.js
new
+184
@@ -0,0 +1,184 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import invariant from 'fbjs/lib/invariant';
11
+
12
+let invokeGuardedCallback = function<A, B, C, D, E, F, Context>(
13
+ name: string | null,
14
+ func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
15
+ context: Context,
16
+ a: A,
17
+ b: B,
18
+ c: C,
19
+ d: D,
20
+ e: E,
21
+ f: F,
22
+) {
23
+ this._hasCaughtError = false;
24
+ this._caughtError = null;
25
+ const funcArgs = Array.prototype.slice.call(arguments, 3);
26
+ try {
27
+ func.apply(context, funcArgs);
28
+ } catch (error) {
29
+ this._caughtError = error;
30
+ this._hasCaughtError = true;
31
+ }
32
+};
33
+
34
+if (__DEV__) {
35
+ // In DEV mode, we swap out invokeGuardedCallback for a special version
36
+ // that plays more nicely with the browser's DevTools. The idea is to preserve
37
+ // "Pause on exceptions" behavior. Because React wraps all user-provided
38
+ // functions in invokeGuardedCallback, and the production version of
39
+ // invokeGuardedCallback uses a try-catch, all user exceptions are treated
40
+ // like caught exceptions, and the DevTools won't pause unless the developer
41
+ // takes the extra step of enabling pause on caught exceptions. This is
42
+ // untintuitive, though, because even though React has caught the error, from
43
+ // the developer's perspective, the error is uncaught.
44
+ //
45
+ // To preserve the expected "Pause on exceptions" behavior, we don't use a
46
+ // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
47
+ // DOM node, and call the user-provided callback from inside an event handler
48
+ // for that fake event. If the callback throws, the error is "captured" using
49
+ // a global event handler. But because the error happens in a different
50
+ // event loop context, it does not interrupt the normal program flow.
51
+ // Effectively, this gives us try-catch behavior without actually using
52
+ // try-catch. Neat!
53
+
54
+ // Check that the browser supports the APIs we need to implement our special
55
+ // DEV version of invokeGuardedCallback
56
+ if (
57
+ typeof window !== 'undefined' &&
58
+ typeof window.dispatchEvent === 'function' &&
59
+ typeof document !== 'undefined' &&
60
+ typeof document.createEvent === 'function'
61
+ ) {
62
+ const fakeNode = document.createElement('react');
63
+
64
+ const invokeGuardedCallbackDev = function<A, B, C, D, E, F, Context>(
65
+ name: string | null,
66
+ func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
67
+ context: Context,
68
+ a: A,
69
+ b: B,
70
+ c: C,
71
+ d: D,
72
+ e: E,
73
+ f: F,
74
+ ) {
75
+ // If document doesn't exist we know for sure we will crash in this method
76
+ // when we call document.createEvent(). However this can cause confusing
77
+ // errors: https://github.com/facebookincubator/create-react-app/issues/3482
78
+ // So we preemptively throw with a better message instead.
79
+ invariant(
80
+ typeof document !== 'undefined',
81
+ 'The `document` global was defined when React was initialized, but is not ' +
82
+ 'defined anymore. This can happen in a test environment if a component ' +
83
+ 'schedules an update from an asynchronous callback, but the test has already ' +
84
+ 'finished running. To solve this, you can either unmount the component at ' +
85
+ 'the end of your test (and ensure that any asynchronous operations get ' +
86
+ 'canceled in `componentWillUnmount`), or you can change the test itself ' +
87
+ 'to be asynchronous.',
88
+ );
89
+ const evt = document.createEvent('Event');
90
+
91
+ // Keeps track of whether the user-provided callback threw an error. We
92
+ // set this to true at the beginning, then set it to false right after
93
+ // calling the function. If the function errors, `didError` will never be
94
+ // set to false. This strategy works even if the browser is flaky and
95
+ // fails to call our global error handler, because it doesn't rely on
96
+ // the error event at all.
97
+ let didError = true;
98
+
99
+ // Create an event handler for our fake event. We will synchronously
100
+ // dispatch our fake event using `dispatchEvent`. Inside the handler, we
101
+ // call the user-provided callback.
102
+ const funcArgs = Array.prototype.slice.call(arguments, 3);
103
+ function callCallback() {
104
+ // We immediately remove the callback from event listeners so that
105
+ // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
106
+ // nested call would trigger the fake event handlers of any call higher
107
+ // in the stack.
108
+ fakeNode.removeEventListener(evtType, callCallback, false);
109
+ func.apply(context, funcArgs);
110
+ didError = false;
111
+ }
112
+
113
+ // Create a global error event handler. We use this to capture the value
114
+ // that was thrown. It's possible that this error handler will fire more
115
+ // than once; for example, if non-React code also calls `dispatchEvent`
116
+ // and a handler for that event throws. We should be resilient to most of
117
+ // those cases. Even if our error event handler fires more than once, the
118
+ // last error event is always used. If the callback actually does error,
119
+ // we know that the last error event is the correct one, because it's not
120
+ // possible for anything else to have happened in between our callback
121
+ // erroring and the code that follows the `dispatchEvent` call below. If
122
+ // the callback doesn't error, but the error event was fired, we know to
123
+ // ignore it because `didError` will be false, as described above.
124
+ let error;
125
+ // Use this to track whether the error event is ever called.
126
+ let didSetError = false;
127
+ let isCrossOriginError = false;
128
+
129
+ function onError(event) {
130
+ error = event.error;
131
+ didSetError = true;
132
+ if (error === null && event.colno === 0 && event.lineno === 0) {
133
+ isCrossOriginError = true;
134
+ }
135
+ }
136
+
137
+ // Create a fake event type.
138
+ const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
139
+
140
+ // Attach our event handlers
141
+ window.addEventListener('error', onError);
142
+ fakeNode.addEventListener(evtType, callCallback, false);
143
+
144
+ // Synchronously dispatch our fake event. If the user-provided function
145
+ // errors, it will trigger our global error handler.
146
+ evt.initEvent(evtType, false, false);
147
+ fakeNode.dispatchEvent(evt);
148
+
149
+ if (didError) {
150
+ if (!didSetError) {
151
+ // The callback errored, but the error event never fired.
152
+ error = new Error(
153
+ 'An error was thrown inside one of your components, but React ' +
154
+ "doesn't know what it was. This is likely due to browser " +
155
+ 'flakiness. React does its best to preserve the "Pause on ' +
156
+ 'exceptions" behavior of the DevTools, which requires some ' +
157
+ "DEV-mode only tricks. It's possible that these don't work in " +
158
+ 'your browser. Try triggering the error in production mode, ' +
159
+ 'or switching to a modern browser. If you suspect that this is ' +
160
+ 'actually an issue with React, please file an issue.',
161
+ );
162
+ } else if (isCrossOriginError) {
163
+ error = new Error(
164
+ "A cross-origin error was thrown. React doesn't have access to " +
165
+ 'the actual error object in development. ' +
166
+ 'See https://fb.me/react-crossorigin-error for more information.',
167
+ );
168
+ }
169
+ this._hasCaughtError = true;
170
+ this._caughtError = error;
171
+ } else {
172
+ this._hasCaughtError = false;
173
+ this._caughtError = null;
174
+ }
175
+
176
+ // Remove our event listeners
177
+ window.removeEventListener('error', onError);
178
+ };
179
+
180
+ invokeGuardedCallback = invokeGuardedCallbackDev;
181
+ }
182
+}
183
+
184
+export default invokeGuardedCallback;
scripts/rollup/forks.js
+12
-1
@@ -69,7 +69,18 @@ const forks = Object.freeze({
69
}
70
},
71
72
- // Different behavior for caught errors.
72
+ // Different wrapping/reporting for caught errors.
73
+ 'shared/invokeGuardedCallback': (bundleType, entry) => {
74
+ switch (bundleType) {
75
+ case FB_DEV:
76
+ case FB_PROD:
77
+ return 'shared/forks/invokeGuardedCallback.www.js';
78
+ default:
79
+ return null;
80
+ }
81
+ },
82
+
83
+ // Different dialogs for caught errors.
84
'react-reconciler/src/ReactFiberErrorDialog': (bundleType, entry) => {
85
switch (bundleType) {
86
case FB_DEV: