Remove invokeGuardedCallback and replay trick (#28515)
We broke the ability to "break on uncaught exceptions" by adding a try/catch higher up in the scheduling. We're giving up on fixing that so we can remove the replay trick inside an event handler. The issue with that approach is that we end up double logging a lot of errors in DEV since they get reported to the page. It's also a lot of complexity around this feature.
Sebastian Markbåge committed
Mar 11, 2024 at 17:17 UTC
89021fb4ec9aa82194b0788566e736a4cedfc0e4
60 files changed
+512
-1685
fixtures/dom/src/toWarnDev.js
-5
@@ -7,11 +7,6 @@ const util = require('util');
7
function shouldIgnoreConsoleError(format, args) {
8
if (__DEV__) {
9
if (typeof format === 'string') {
10
- if (format.indexOf('Error: Uncaught [') === 0) {
11
- // This looks like an uncaught error from invokeGuardedCallback() wrapper
12
- // in development that is reported by jsdom. Ignore because it's noisy.
13
- return true;
14
- }
10
if (format.indexOf('The above error occurred') === 0) {
11
// This looks like an error addendum from ReactFiberErrorLogger.
12
// Ignore it too.
packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js
-4
@@ -12,7 +12,6 @@
12
let ReactCache;
13
let createResource;
14
let React;
15
-let ReactFeatureFlags;
15
let ReactNoop;
16
let Scheduler;
17
let Suspense;
@@ -27,9 +26,6 @@ describe('ReactCache', () => {
26
beforeEach(() => {
27
jest.resetModules();
28
30
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
31
-
32
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
29
React = require('react');
30
Suspense = React.Suspense;
31
ReactCache = require('react-cache');
packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js
-4
@@ -870,7 +870,6 @@ describe('Timeline profiler', () => {
870
"--component-render-start-ErrorBoundary",
871
"--component-render-stop",
872
"--component-render-start-ExampleThatThrows",
873
- "--component-render-start-ExampleThatThrows",
873
"--component-render-stop",
874
"--error-ExampleThatThrows-mount-Expected error",
875
"--render-stop",
@@ -878,7 +877,6 @@ describe('Timeline profiler', () => {
877
"--component-render-start-ErrorBoundary",
878
"--component-render-stop",
879
"--component-render-start-ExampleThatThrows",
881
- "--component-render-start-ExampleThatThrows",
880
"--component-render-stop",
881
"--error-ExampleThatThrows-mount-Expected error",
882
"--render-stop",
@@ -2161,10 +2159,8 @@ describe('Timeline profiler', () => {
2159
await waitForAll([
2160
'ErrorBoundary render',
2161
'ExampleThatThrows',
2164
- 'ExampleThatThrows',
2162
'ErrorBoundary render',
2163
'ExampleThatThrows',
2167
- 'ExampleThatThrows',
2164
'ErrorBoundary fallback',
2165
]);
2166
packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
+19
-7
@@ -55,10 +55,6 @@ import {
55
enableFloat,
56
enableFormActions,
57
} from 'shared/ReactFeatureFlags';
58
-import {
59
- invokeGuardedCallbackAndCatchFirstError,
60
- rethrowCaughtError,
61
-} from 'shared/ReactErrorUtils';
58
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
59
import {
60
removeEventListener,
@@ -234,14 +230,25 @@ export const nonDelegatedEvents: Set<DOMEventName> = new Set([
230
...mediaEventTypes,
231
]);
232
233
+let hasError: boolean = false;
234
+let caughtError: mixed = null;
235
+
236
function executeDispatch(
237
event: ReactSyntheticEvent,
238
listener: Function,
239
currentTarget: EventTarget,
240
): void {
242
- const type = event.type || 'unknown-event';
241
event.currentTarget = currentTarget;
244
- invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
242
+ try {
243
+ listener(event);
244
+ } catch (error) {
245
+ if (!hasError) {
246
+ hasError = true;
247
+ caughtError = error;
248
+ } else {
249
+ // TODO: Make sure this error gets logged somehow.
250
+ }
251
+ }
252
event.currentTarget = null;
253
}
254
@@ -283,7 +290,12 @@ export function processDispatchQueue(
290
// event system doesn't use pooling.
291
}
292
// This would be a good time to rethrow if any of the event handlers threw.
286
- rethrowCaughtError();
293
+ if (hasError) {
294
+ const error = caughtError;
295
+ hasError = false;
296
+ caughtError = null;
297
+ throw error;
298
+ }
299
}
300
301
function dispatchEventsForPlugins(
packages/react-dom/src/__tests__/InvalidEventListeners-test.js
+10
-12
@@ -43,7 +43,7 @@ describe('InvalidEventListeners', () => {
43
);
44
const node = container.firstChild;
45
46
- spyOnProd(console, 'error');
46
+ console.error = jest.fn();
47
48
const uncaughtErrors = [];
49
function handleWindowError(e) {
@@ -70,18 +70,16 @@ describe('InvalidEventListeners', () => {
70
}),
71
);
72
73
- if (!__DEV__) {
74
- expect(console.error).toHaveBeenCalledTimes(1);
75
- expect(console.error.mock.calls[0][0]).toEqual(
76
- expect.objectContaining({
77
- detail: expect.objectContaining({
78
- message:
79
- 'Expected `onClick` listener to be a function, instead got a value of `string` type.',
80
- }),
81
- type: 'unhandled exception',
73
+ expect(console.error).toHaveBeenCalledTimes(1);
74
+ expect(console.error.mock.calls[0][0]).toEqual(
75
+ expect.objectContaining({
76
+ detail: expect.objectContaining({
77
+ message:
78
+ 'Expected `onClick` listener to be a function, instead got a value of `string` type.',
79
}),
83
- );
84
- }
80
+ type: 'unhandled exception',
81
+ }),
82
+ );
83
});
84
85
it('should not prevent null listeners, at dispatch', async () => {
packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.js
+1
-9
@@ -202,21 +202,13 @@ describe('ReactBrowserEventEmitter', () => {
202
expect(idCallOrder[0]).toBe(CHILD);
203
expect(idCallOrder[1]).toBe(PARENT);
204
expect(idCallOrder[2]).toBe(GRANDPARENT);
205
- expect(errorHandler).toHaveBeenCalledTimes(__DEV__ ? 2 : 1);
205
+ expect(errorHandler).toHaveBeenCalledTimes(1);
206
expect(errorHandler.mock.calls[0][0]).toEqual(
207
expect.objectContaining({
208
error: expect.any(Error),
209
message: 'Handler interrupted',
210
}),
211
);
212
- if (__DEV__) {
213
- expect(errorHandler.mock.calls[1][0]).toEqual(
214
- expect.objectContaining({
215
- error: expect.any(Error),
216
- message: 'Handler interrupted',
217
- }),
218
- );
219
- }
212
} finally {
213
window.removeEventListener('error', errorHandler);
214
}
packages/react-dom/src/__tests__/ReactCompositeComponent-test.js
-15
@@ -1231,13 +1231,6 @@ describe('ReactCompositeComponent', () => {
1231
});
1232
}).toThrow();
1233
}).toErrorDev([
1234
- // Expect two errors because invokeGuardedCallback will dispatch an error event,
1235
- // Causing the warning to be logged again.
1236
- 'Warning: No `render` method found on the RenderTextInvalidConstructor instance: ' +
1237
- 'did you accidentally return an object from the constructor?',
1238
- 'Warning: No `render` method found on the RenderTextInvalidConstructor instance: ' +
1239
- 'did you accidentally return an object from the constructor?',
1240
- // And then two more because we retry errors.
1234
'Warning: No `render` method found on the RenderTextInvalidConstructor instance: ' +
1235
'did you accidentally return an object from the constructor?',
1236
'Warning: No `render` method found on the RenderTextInvalidConstructor instance: ' +
@@ -1278,14 +1271,6 @@ describe('ReactCompositeComponent', () => {
1271
});
1272
}).toThrow();
1273
}).toErrorDev([
1281
- // Expect two errors because invokeGuardedCallback will dispatch an error event,
1282
- // Causing the warning to be logged again.
1283
- 'Warning: No `render` method found on the RenderTestUndefinedRender instance: ' +
1284
- 'you may have forgotten to define `render`.',
1285
- 'Warning: No `render` method found on the RenderTestUndefinedRender instance: ' +
1286
- 'you may have forgotten to define `render`.',
1287
-
1288
- // And then two more because we retry errors.
1274
'Warning: No `render` method found on the RenderTestUndefinedRender instance: ' +
1275
'you may have forgotten to define `render`.',
1276
'Warning: No `render` method found on the RenderTestUndefinedRender instance: ' +
packages/react-dom/src/__tests__/ReactDOMConsoleErrorReporting-test.js
+62
-191
@@ -56,7 +56,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
56
57
describe('ReactDOMClient.createRoot', () => {
58
it('logs errors during event handlers', async () => {
59
- spyOnDevAndProd(console, 'error');
59
+ const originalError = console.error;
60
+ console.error = jest.fn();
61
62
function Foo() {
63
return (
@@ -82,76 +83,34 @@ describe('ReactDOMConsoleErrorReporting', () => {
83
);
84
});
85
85
- if (__DEV__) {
86
- expect(windowOnError.mock.calls).toEqual([
87
- [
88
- // Reported because we're in a browser click event:
89
- expect.objectContaining({
90
- message: 'Boom',
91
- }),
92
- ],
93
- [
94
- // This one is jsdom-only. Real browser deduplicates it.
95
- // (In DEV, we have a nested event due to guarded callback.)
96
- expect.objectContaining({
97
- message: 'Boom',
98
- }),
99
- ],
100
- ]);
101
- expect(console.error.mock.calls).toEqual([
102
- [
103
- // Reported because we're in a browser click event:
104
- expect.objectContaining({
105
- detail: expect.objectContaining({
106
- message: 'Boom',
107
- }),
108
- type: 'unhandled exception',
109
- }),
110
- ],
111
- [
112
- // This one is jsdom-only. Real browser deduplicates it.
113
- // (In DEV, we have a nested event due to guarded callback.)
114
- expect.objectContaining({
115
- detail: expect.objectContaining({
116
- message: 'Boom',
117
- }),
118
- type: 'unhandled exception',
119
- }),
120
- ],
121
- ]);
122
- } else {
123
- expect(windowOnError.mock.calls).toEqual([
124
- [
125
- // Reported because we're in a browser click event:
126
- expect.objectContaining({
86
+ expect(windowOnError.mock.calls).toEqual([
87
+ [
88
+ // Reported because we're in a browser click event:
89
+ expect.objectContaining({
90
+ message: 'Boom',
91
+ }),
92
+ ],
93
+ ]);
94
+ expect(console.error.mock.calls).toEqual([
95
+ [
96
+ // Reported because we're in a browser click event:
97
+ expect.objectContaining({
98
+ detail: expect.objectContaining({
99
message: 'Boom',
100
}),
129
- ],
130
- ]);
131
- expect(console.error.mock.calls).toEqual([
132
- [
133
- // Reported because we're in a browser click event:
134
- expect.objectContaining({
135
- detail: expect.objectContaining({
136
- message: 'Boom',
137
- }),
138
- type: 'unhandled exception',
139
- }),
140
- ],
141
- ]);
142
- }
101
+ type: 'unhandled exception',
102
+ }),
103
+ ],
104
+ ]);
105
106
// Check next render doesn't throw.
107
windowOnError.mockReset();
146
- console.error.mockReset();
108
+ console.error = originalError;
109
await act(() => {
110
root.render(<NoError />);
111
});
112
expect(container.textContent).toBe('OK');
113
expect(windowOnError.mock.calls).toEqual([]);
152
- if (__DEV__) {
153
- expect(console.error.mock.calls).toEqual([]);
154
- }
114
});
115
116
it('logs render errors without an error boundary', async () => {
@@ -168,50 +127,24 @@ describe('ReactDOMConsoleErrorReporting', () => {
127
});
128
129
if (__DEV__) {
171
- expect(windowOnError.mock.calls).toEqual([
172
- [
173
- // Reported due to guarded callback:
174
- expect.objectContaining({
175
- message: 'Boom',
176
- }),
177
- ],
178
- [
179
- // This is only duplicated with createRoot
180
- // because it retries once with a sync render.
181
- expect.objectContaining({
182
- message: 'Boom',
183
- }),
184
- ],
185
- ]);
130
+ expect(windowOnError.mock.calls).toEqual([]);
131
expect(console.error.mock.calls).toEqual([
132
[
188
- // Reported due to the guarded callback:
133
+ // Formatting
134
+ expect.stringContaining('%o'),
135
expect.objectContaining({
190
- detail: expect.objectContaining({
191
- message: 'Boom',
192
- }),
193
- type: 'unhandled exception',
194
- }),
195
- ],
196
- [
197
- // This is only duplicated with createRoot
198
- // because it retries once with a sync render.
199
- expect.objectContaining({
200
- detail: expect.objectContaining({
201
- message: 'Boom',
202
- }),
203
- type: 'unhandled exception',
136
+ message: 'Boom',
137
}),
205
- ],
206
- [
138
// Addendum by React:
139
expect.stringContaining(
140
'The above error occurred in the <Foo> component',
141
),
142
+ expect.stringContaining('Foo'),
143
+ expect.stringContaining('Consider adding an error boundary'),
144
],
145
]);
146
} else {
214
- // The top-level error was caught with try/catch, and there's no guarded callback,
147
+ // The top-level error was caught with try/catch,
148
// so in production we don't see an error event.
149
expect(windowOnError.mock.calls).toEqual([]);
150
expect(console.error.mock.calls).toEqual([
@@ -254,50 +187,24 @@ describe('ReactDOMConsoleErrorReporting', () => {
187
});
188
189
if (__DEV__) {
257
- expect(windowOnError.mock.calls).toEqual([
258
- [
259
- // Reported due to guarded callback:
260
- expect.objectContaining({
261
- message: 'Boom',
262
- }),
263
- ],
264
- [
265
- // This is only duplicated with createRoot
266
- // because it retries once with a sync render.
267
- expect.objectContaining({
268
- message: 'Boom',
269
- }),
270
- ],
271
- ]);
190
+ expect(windowOnError.mock.calls).toEqual([]);
191
expect(console.error.mock.calls).toEqual([
192
[
274
- // Reported by jsdom due to the guarded callback:
275
- expect.objectContaining({
276
- detail: expect.objectContaining({
277
- message: 'Boom',
278
- }),
279
- type: 'unhandled exception',
280
- }),
281
- ],
282
- [
283
- // This is only duplicated with createRoot
284
- // because it retries once with a sync render.
193
+ // Formatting
194
+ expect.stringContaining('%o'),
195
expect.objectContaining({
286
- detail: expect.objectContaining({
287
- message: 'Boom',
288
- }),
289
- type: 'unhandled exception',
196
+ message: 'Boom',
197
}),
291
- ],
292
- [
198
// Addendum by React:
199
expect.stringContaining(
200
'The above error occurred in the <Foo> component',
201
),
202
+ expect.stringContaining('Foo'),
203
+ expect.stringContaining('ErrorBoundary'),
204
],
205
]);
206
} else {
300
- // The top-level error was caught with try/catch, and there's no guarded callback,
207
+ // The top-level error was caught with try/catch,
208
// so in production we don't see an error event.
209
expect(windowOnError.mock.calls).toEqual([]);
210
expect(console.error.mock.calls).toEqual([
@@ -340,33 +247,24 @@ describe('ReactDOMConsoleErrorReporting', () => {
247
});
248
249
if (__DEV__) {
343
- expect(windowOnError.mock.calls).toEqual([
344
- [
345
- // Reported due to guarded callback:
346
- expect.objectContaining({
347
- message: 'Boom',
348
- }),
349
- ],
350
- ]);
250
+ expect(windowOnError.mock.calls).toEqual([]);
251
expect(console.error.mock.calls).toEqual([
252
[
353
- // Reported due to the guarded callback:
253
+ // Formatting
254
+ expect.stringContaining('%o'),
255
expect.objectContaining({
355
- detail: expect.objectContaining({
356
- message: 'Boom',
357
- }),
358
- type: 'unhandled exception',
256
+ message: 'Boom',
257
}),
360
- ],
361
- [
258
// Addendum by React:
259
expect.stringContaining(
260
'The above error occurred in the <Foo> component',
261
),
262
+ expect.stringContaining('Foo'),
263
+ expect.stringContaining('Consider adding an error boundary'),
264
],
265
]);
266
} else {
369
- // The top-level error was caught with try/catch, and there's no guarded callback,
267
+ // The top-level error was caught with try/catch,
268
// so in production we don't see an error event.
269
expect(windowOnError.mock.calls).toEqual([]);
270
expect(console.error.mock.calls).toEqual([
@@ -412,33 +310,24 @@ describe('ReactDOMConsoleErrorReporting', () => {
310
});
311
312
if (__DEV__) {
415
- expect(windowOnError.mock.calls).toEqual([
416
- [
417
- // Reported due to guarded callback:
418
- expect.objectContaining({
419
- message: 'Boom',
420
- }),
421
- ],
422
- ]);
313
+ expect(windowOnError.mock.calls).toEqual([]);
314
expect(console.error.mock.calls).toEqual([
315
[
425
- // Reported by jsdom due to the guarded callback:
316
+ // Formatting
317
+ expect.stringContaining('%o'),
318
expect.objectContaining({
427
- detail: expect.objectContaining({
428
- message: 'Boom',
429
- }),
430
- type: 'unhandled exception',
319
+ message: 'Boom',
320
}),
432
- ],
433
- [
321
// Addendum by React:
322
expect.stringContaining(
323
'The above error occurred in the <Foo> component',
324
),
325
+ expect.stringContaining('Foo'),
326
+ expect.stringContaining('ErrorBoundary'),
327
],
328
]);
329
} else {
441
- // The top-level error was caught with try/catch, and there's no guarded callback,
330
+ // The top-level error was caught with try/catch,
331
// so in production we don't see an error event.
332
expect(windowOnError.mock.calls).toEqual([]);
333
expect(console.error.mock.calls).toEqual([
@@ -481,33 +370,24 @@ describe('ReactDOMConsoleErrorReporting', () => {
370
});
371
372
if (__DEV__) {
484
- expect(windowOnError.mock.calls).toEqual([
485
- [
486
- // Reported due to guarded callback:
487
- expect.objectContaining({
488
- message: 'Boom',
489
- }),
490
- ],
491
- ]);
373
+ expect(windowOnError.mock.calls).toEqual([]);
374
expect(console.error.mock.calls).toEqual([
375
[
494
- // Reported due to the guarded callback:
376
+ // Formatting
377
+ expect.stringContaining('%o'),
378
expect.objectContaining({
496
- detail: expect.objectContaining({
497
- message: 'Boom',
498
- }),
499
- type: 'unhandled exception',
379
+ message: 'Boom',
380
}),
501
- ],
502
- [
381
// Addendum by React:
382
expect.stringContaining(
383
'The above error occurred in the <Foo> component',
384
),
385
+ expect.stringContaining('Foo'),
386
+ expect.stringContaining('Consider adding an error boundary'),
387
],
388
]);
389
} else {
510
- // The top-level error was caught with try/catch, and there's no guarded callback,
390
+ // The top-level error was caught with try/catch,
391
// so in production we don't see an error event.
392
expect(windowOnError.mock.calls).toEqual([]);
393
expect(console.error.mock.calls).toEqual([
@@ -553,33 +433,24 @@ describe('ReactDOMConsoleErrorReporting', () => {
433
});
434
435
if (__DEV__) {
556
- expect(windowOnError.mock.calls).toEqual([
557
- [
558
- // Reported due to guarded callback:
559
- expect.objectContaining({
560
- message: 'Boom',
561
- }),
562
- ],
563
- ]);
436
+ expect(windowOnError.mock.calls).toEqual([]);
437
expect(console.error.mock.calls).toEqual([
438
[
566
- // Reported by jsdom due to the guarded callback:
439
+ // Formatting
440
+ expect.stringContaining('%o'),
441
expect.objectContaining({
568
- detail: expect.objectContaining({
569
- message: 'Boom',
570
- }),
571
- type: 'unhandled exception',
442
+ message: 'Boom',
443
}),
573
- ],
574
- [
444
// Addendum by React:
445
expect.stringContaining(
446
'The above error occurred in the <Foo> component',
447
),
448
+ expect.stringContaining('Foo'),
449
+ expect.stringContaining('ErrorBoundary'),
450
],
451
]);
452
} else {
582
- // The top-level error was caught with try/catch, and there's no guarded callback,
453
+ // The top-level error was caught with try/catch,
454
// so in production we don't see an error event.
455
expect(windowOnError.mock.calls).toEqual([]);
456
expect(console.error.mock.calls).toEqual([
packages/react-dom/src/__tests__/ReactDOMConsoleErrorReportingLegacy-test.js
+45
-114
@@ -57,7 +57,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
57
describe('ReactDOM.render', () => {
58
// @gate !disableLegacyMode
59
it('logs errors during event handlers', async () => {
60
- spyOnDevAndProd(console, 'error');
60
+ const originalError = console.error;
61
+ console.error = jest.fn();
62
63
function Foo() {
64
return (
@@ -90,13 +91,6 @@ describe('ReactDOMConsoleErrorReporting', () => {
91
message: 'Boom',
92
}),
93
],
93
- [
94
- // This one is jsdom-only. Real browser deduplicates it.
95
- // (In DEV, we have a nested event due to guarded callback.)
96
- expect.objectContaining({
97
- message: 'Boom',
98
- }),
99
- ],
94
]);
95
expect(console.error.mock.calls).toEqual([
96
[expect.stringContaining('ReactDOM.render is no longer supported')],
@@ -109,16 +103,6 @@ describe('ReactDOMConsoleErrorReporting', () => {
103
type: 'unhandled exception',
104
}),
105
],
112
- [
113
- // This one is jsdom-only. Real browser deduplicates it.
114
- // (In DEV, we have a nested event due to guarded callback.)
115
- expect.objectContaining({
116
- detail: expect.objectContaining({
117
- message: 'Boom',
118
- }),
119
- type: 'unhandled exception',
120
- }),
121
- ],
106
]);
107
} else {
108
expect(windowOnError.mock.calls).toEqual([
@@ -155,6 +139,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
139
[expect.stringContaining('ReactDOM.render is no longer supported')],
140
]);
141
}
142
+
143
+ console.error = originalError;
144
});
145
146
// @gate !disableLegacyMode
@@ -170,34 +156,24 @@ describe('ReactDOMConsoleErrorReporting', () => {
156
}).toThrow('Boom');
157
158
if (__DEV__) {
173
- expect(windowOnError.mock.calls).toEqual([
174
- [
175
- // Reported due to guarded callback:
176
- expect.objectContaining({
177
- message: 'Boom',
178
- }),
179
- ],
180
- ]);
159
expect(console.error.mock.calls).toEqual([
160
[expect.stringContaining('ReactDOM.render is no longer supported')],
161
[
184
- // Reported due to the guarded callback:
162
+ // Formatting
163
+ expect.stringContaining('%o'),
164
expect.objectContaining({
186
- detail: expect.objectContaining({
187
- message: 'Boom',
188
- }),
189
- type: 'unhandled exception',
165
+ message: 'Boom',
166
}),
191
- ],
192
- [
167
// Addendum by React:
168
expect.stringContaining(
169
'The above error occurred in the <Foo> component',
170
),
171
+ expect.stringContaining('Foo'),
172
+ expect.stringContaining('Consider adding an error boundary'),
173
],
174
]);
175
} else {
200
- // The top-level error was caught with try/catch, and there's no guarded callback,
176
+ // The top-level error was caught with try/catch,
177
// so in production we don't see an error event.
178
expect(windowOnError.mock.calls).toEqual([]);
179
expect(console.error.mock.calls).toEqual([
@@ -243,34 +219,25 @@ describe('ReactDOMConsoleErrorReporting', () => {
219
});
220
221
if (__DEV__) {
246
- expect(windowOnError.mock.calls).toEqual([
247
- [
248
- // Reported due to guarded callback:
249
- expect.objectContaining({
250
- message: 'Boom',
251
- }),
252
- ],
253
- ]);
222
+ expect(windowOnError.mock.calls).toEqual([]);
223
expect(console.error.mock.calls).toEqual([
224
[expect.stringContaining('ReactDOM.render is no longer supported')],
225
[
257
- // Reported by jsdom due to the guarded callback:
226
+ // Formatting
227
+ expect.stringContaining('%o'),
228
expect.objectContaining({
259
- detail: expect.objectContaining({
260
- message: 'Boom',
261
- }),
262
- type: 'unhandled exception',
229
+ message: 'Boom',
230
}),
264
- ],
265
- [
231
// Addendum by React:
232
expect.stringContaining(
233
'The above error occurred in the <Foo> component',
234
),
235
+ expect.stringContaining('Foo'),
236
+ expect.stringContaining('ErrorBoundary'),
237
],
238
]);
239
} else {
273
- // The top-level error was caught with try/catch, and there's no guarded callback,
240
+ // The top-level error was caught with try/catch,
241
// so in production we don't see an error event.
242
expect(windowOnError.mock.calls).toEqual([]);
243
expect(console.error.mock.calls).toEqual([
@@ -314,34 +281,25 @@ describe('ReactDOMConsoleErrorReporting', () => {
281
}).toThrow('Boom');
282
283
if (__DEV__) {
317
- expect(windowOnError.mock.calls).toEqual([
318
- [
319
- // Reported due to guarded callback:
320
- expect.objectContaining({
321
- message: 'Boom',
322
- }),
323
- ],
324
- ]);
284
+ expect(windowOnError.mock.calls).toEqual([]);
285
expect(console.error.mock.calls).toEqual([
286
[expect.stringContaining('ReactDOM.render is no longer supported')],
287
[
328
- // Reported due to the guarded callback:
288
+ // Formatting
289
+ expect.stringContaining('%o'),
290
expect.objectContaining({
330
- detail: expect.objectContaining({
331
- message: 'Boom',
332
- }),
333
- type: 'unhandled exception',
291
+ message: 'Boom',
292
}),
335
- ],
336
- [
293
// Addendum by React:
294
expect.stringContaining(
295
'The above error occurred in the <Foo> component',
296
),
297
+ expect.stringContaining('Foo'),
298
+ expect.stringContaining('Consider adding an error boundary'),
299
],
300
]);
301
} else {
344
- // The top-level error was caught with try/catch, and there's no guarded callback,
302
+ // The top-level error was caught with try/catch,
303
// so in production we don't see an error event.
304
expect(windowOnError.mock.calls).toEqual([]);
305
expect(console.error.mock.calls).toEqual([
@@ -390,34 +348,25 @@ describe('ReactDOMConsoleErrorReporting', () => {
348
});
349
350
if (__DEV__) {
393
- expect(windowOnError.mock.calls).toEqual([
394
- [
395
- // Reported due to guarded callback:
396
- expect.objectContaining({
397
- message: 'Boom',
398
- }),
399
- ],
400
- ]);
351
+ expect(windowOnError.mock.calls).toEqual([]);
352
expect(console.error.mock.calls).toEqual([
353
[expect.stringContaining('ReactDOM.render is no longer supported')],
354
[
404
- // Reported by jsdom due to the guarded callback:
355
+ // Formatting
356
+ expect.stringContaining('%o'),
357
expect.objectContaining({
406
- detail: expect.objectContaining({
407
- message: 'Boom',
408
- }),
409
- type: 'unhandled exception',
358
+ message: 'Boom',
359
}),
411
- ],
412
- [
360
// Addendum by React:
361
expect.stringContaining(
362
'The above error occurred in the <Foo> component',
363
),
364
+ expect.stringContaining('Foo'),
365
+ expect.stringContaining('ErrorBoundary'),
366
],
367
]);
368
} else {
420
- // The top-level error was caught with try/catch, and there's no guarded callback,
369
+ // The top-level error was caught with try/catch,
370
// so in production we don't see an error event.
371
expect(windowOnError.mock.calls).toEqual([]);
372
expect(console.error.mock.calls).toEqual([
@@ -462,34 +411,25 @@ describe('ReactDOMConsoleErrorReporting', () => {
411
});
412
413
if (__DEV__) {
465
- expect(windowOnError.mock.calls).toEqual([
466
- [
467
- // Reported due to guarded callback:
468
- expect.objectContaining({
469
- message: 'Boom',
470
- }),
471
- ],
472
- ]);
414
+ expect(windowOnError.mock.calls).toEqual([]);
415
expect(console.error.mock.calls).toEqual([
416
[expect.stringContaining('ReactDOM.render is no longer supported')],
417
[
476
- // Reported due to the guarded callback:
418
+ // Formatting
419
+ expect.stringContaining('%o'),
420
expect.objectContaining({
478
- detail: expect.objectContaining({
479
- message: 'Boom',
480
- }),
481
- type: 'unhandled exception',
421
+ message: 'Boom',
422
}),
483
- ],
484
- [
423
// Addendum by React:
424
expect.stringContaining(
425
'The above error occurred in the <Foo> component',
426
),
427
+ expect.stringContaining('Foo'),
428
+ expect.stringContaining('Consider adding an error boundary'),
429
],
430
]);
431
} else {
492
- // The top-level error was caught with try/catch, and there's no guarded callback,
432
+ // The top-level error was caught with try/catch,
433
// so in production we don't see an error event.
434
expect(windowOnError.mock.calls).toEqual([]);
435
expect(console.error.mock.calls).toEqual([
@@ -538,34 +478,25 @@ describe('ReactDOMConsoleErrorReporting', () => {
478
});
479
480
if (__DEV__) {
541
- // Reported due to guarded callback:
542
- expect(windowOnError.mock.calls).toEqual([
543
- [
544
- expect.objectContaining({
545
- message: 'Boom',
546
- }),
547
- ],
548
- ]);
481
+ expect(windowOnError.mock.calls).toEqual([]);
482
expect(console.error.mock.calls).toEqual([
483
[expect.stringContaining('ReactDOM.render is no longer supported')],
484
[
552
- // Reported by jsdom due to the guarded callback:
485
+ // Formatting
486
+ expect.stringContaining('%o'),
487
expect.objectContaining({
554
- detail: expect.objectContaining({
555
- message: 'Boom',
556
- }),
557
- type: 'unhandled exception',
488
+ message: 'Boom',
489
}),
559
- ],
560
- [
490
// Addendum by React:
491
expect.stringContaining(
492
'The above error occurred in the <Foo> component',
493
),
494
+ expect.stringContaining('Foo'),
495
+ expect.stringContaining('ErrorBoundary'),
496
],
497
]);
498
} else {
568
- // The top-level error was caught with try/catch, and there's no guarded callback,
499
+ // The top-level error was caught with try/catch,
500
// so in production we don't see an error event.
501
expect(windowOnError.mock.calls).toEqual([]);
502
expect(console.error.mock.calls).toEqual([
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+6
-26
@@ -3234,7 +3234,7 @@ describe('ReactDOMFizzServer', () => {
3234
it('logs regular (non-hydration) errors when the UI recovers', async () => {
3235
let shouldThrow = true;
3236
3237
- function A() {
3237
+ function A({unused}) {
3238
if (shouldThrow) {
3239
Scheduler.log('Oops!');
3240
throw new Error('Oops!');
@@ -3267,7 +3267,7 @@ describe('ReactDOMFizzServer', () => {
3267
});
3268
3269
// Partially render A, but yield before the render has finished
3270
- await waitFor(['Oops!', 'Oops!']);
3270
+ await waitFor(['Oops!']);
3271
3272
// React will try rendering again synchronously. During the retry, A will
3273
// not throw. This simulates a concurrent data race that is fixed by
@@ -4643,8 +4643,7 @@ describe('ReactDOMFizzServer', () => {
4643
await waitForAll([]);
4644
});
4645
4646
- // @gate __DEV__
4647
- it('does not invokeGuardedCallback for errors after the first hydration error', async () => {
4646
+ it('does not log for errors after the first hydration error', async () => {
4647
// We can't use the toErrorDev helper here because this is async.
4648
const originalConsoleError = console.error;
4649
const mockError = jest.fn();
@@ -4717,21 +4716,13 @@ describe('ReactDOMFizzServer', () => {
4716
});
4717
await waitForAll([
4718
'throwing: first error',
4720
- // this repeated first error is the invokeGuardedCallback throw
4721
- 'throwing: first error',
4719
4720
// onRecoverableError because the UI recovered without surfacing the
4721
// error to the user.
4722
'Logged recoverable error: first error',
4723
'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
4724
]);
4728
- // These Uncaught error calls are the error reported by the runtime (jsdom here, browser in actual use)
4729
- // when invokeGuardedCallback is used to replay an error in dev using event dispatching in the document
4730
- expect(mockError.mock.calls).toEqual([
4731
- // we only get one because we suppress invokeGuardedCallback after the first one when hydrating in a
4732
- // suspense boundary
4733
- ['Error: Uncaught [Error: first error]'],
4734
- ]);
4725
+ expect(mockError.mock.calls).toEqual([]);
4726
mockError.mockClear();
4727
4728
expect(getVisibleChildren(container)).toEqual(
@@ -4749,8 +4740,7 @@ describe('ReactDOMFizzServer', () => {
4740
}
4741
});
4742
4752
- // @gate __DEV__
4753
- it('does not invokeGuardedCallback for errors after a preceding fiber suspends', async () => {
4743
+ it('does not log for errors after a preceding fiber suspends', async () => {
4744
// We can't use the toErrorDev helper here because this is async.
4745
const originalConsoleError = console.error;
4746
const mockError = jest.fn();
@@ -4853,7 +4843,6 @@ describe('ReactDOMFizzServer', () => {
4843
);
4844
await unsuspend();
4845
await waitForAll([
4856
- 'throwing: first error',
4846
'throwing: first error',
4847
'Logged recoverable error: first error',
4848
'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
@@ -4870,7 +4859,6 @@ describe('ReactDOMFizzServer', () => {
4859
}
4860
});
4861
4873
- // @gate __DEV__
4862
it('(outdated behavior) suspending after erroring will cause errors previously queued to be silenced until the boundary resolves', async () => {
4863
// NOTE: This test was originally written to test a scenario that doesn't happen
4864
// anymore. If something errors during hydration, we immediately unwind the
@@ -4968,20 +4956,12 @@ describe('ReactDOMFizzServer', () => {
4956
},
4957
});
4958
await waitForAll([
4971
- 'throwing: first error',
4972
- // duplicate because first error is re-done in invokeGuardedCallback
4959
'throwing: first error',
4960
'suspending',
4961
'Logged recoverable error: first error',
4962
'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
4963
]);
4978
- // These Uncaught error calls are the error reported by the runtime (jsdom here, browser in actual use)
4979
- // when invokeGuardedCallback is used to replay an error in dev using event dispatching in the document
4980
- expect(mockError.mock.calls).toEqual([
4981
- // we only get one because we suppress invokeGuardedCallback after the first one when hydrating in a
4982
- // suspense boundary
4983
- ['Error: Uncaught [Error: first error]'],
4984
- ]);
4964
+ expect(mockError.mock.calls).toEqual([]);
4965
mockError.mockClear();
4966
4967
expect(getVisibleChildren(container)).toEqual(
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
-9
@@ -51,15 +51,6 @@ describe('ReactDOMServerHydration', () => {
51
if (format instanceof Error) {
52
return 'Caught [' + format.message + ']';
53
}
54
- if (
55
- format !== null &&
56
- typeof format === 'object' &&
57
- String(format).indexOf('Error: Uncaught [') === 0
58
- ) {
59
- // Ignore errors captured by jsdom and their stacks.
60
- // We only want console errors in this suite.
61
- return null;
62
- }
54
rest[rest.length - 1] = normalizeCodeLocInfo(rest[rest.length - 1]);
55
return util.format(format, ...rest);
56
}
packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js
+1
-1
@@ -278,7 +278,7 @@ describe('ReactDOMServerIntegration', () => {
278
}
279
const e = await render(
280
<ForgetfulParent />,
281
- render === clientRenderOnBadMarkup ? 4 : 1,
281
+ render === clientRenderOnBadMarkup ? 2 : 1,
282
);
283
expect(e.textContent).toBe('nope');
284
},
packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js
+1
-1
@@ -105,7 +105,7 @@ describe('ReactDOMServerIntegrationLegacyContextDisabled', () => {
105
<RegularFn />
106
</span>
107
</LegacyProvider>,
108
- render === clientRenderOnBadMarkup ? 6 : 3,
108
+ render === clientRenderOnBadMarkup ? 5 : 3,
109
);
110
expect(e.textContent).toBe('{}undefinedundefined');
111
expect(lifecycleContextLog).toEqual([]);
packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js
+2
-2
@@ -254,7 +254,7 @@ describe('ReactDOMServerIntegrationSelect', () => {
254
<option value="first">First</option>
255
<option value="true">True</option>
256
</select>,
257
- 2,
257
+ 1,
258
);
259
expect(e.firstChild.selected).toBe(false);
260
expect(e.lastChild.selected).toBe(true);
@@ -269,7 +269,7 @@ describe('ReactDOMServerIntegrationSelect', () => {
269
<option value="first">First</option>
270
<option value="undefined">Undefined</option>
271
</select>,
272
- 2,
272
+ 1,
273
);
274
expect(e.firstChild.selected).toBe(true);
275
expect(e.lastChild.selected).toBe(false);
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+2
-3
@@ -349,9 +349,8 @@ describe('ReactDOMServerPartialHydration', () => {
349
);
350
351
if (__DEV__) {
352
- const secondToLastCall =
353
- mockError.mock.calls[mockError.mock.calls.length - 2];
354
- expect(secondToLastCall).toEqual([
352
+ const lastCall = mockError.mock.calls[mockError.mock.calls.length - 1];
353
+ expect(lastCall).toEqual([
354
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
355
'article',
356
'section',
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+1
-4
@@ -14,7 +14,6 @@ let React;
14
let ReactDOM;
15
let ReactDOMClient;
16
let act;
17
-let ReactFeatureFlags;
17
let Scheduler;
18
19
describe('ReactErrorBoundaries', () => {
@@ -42,8 +41,6 @@ describe('ReactErrorBoundaries', () => {
41
jest.useFakeTimers();
42
jest.resetModules();
43
PropTypes = require('prop-types');
45
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
46
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
44
ReactDOM = require('react-dom');
45
ReactDOMClient = require('react-dom/client');
46
React = require('react');
@@ -710,7 +707,7 @@ describe('ReactErrorBoundaries', () => {
707
});
708
if (__DEV__) {
709
expect(console.error).toHaveBeenCalledTimes(1);
713
- expect(console.error.mock.calls[0][0]).toContain(
710
+ expect(console.error.mock.calls[0][2]).toContain(
711
'The above error occurred in the <BrokenRender> component:',
712
);
713
}
packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
+1
-4
@@ -12,7 +12,6 @@
12
let PropTypes;
13
let React;
14
let ReactDOM;
15
-let ReactFeatureFlags;
15
16
// TODO: Refactor this test once componentDidCatch setState is deprecated.
17
describe('ReactLegacyErrorBoundaries', () => {
@@ -39,8 +38,6 @@ describe('ReactLegacyErrorBoundaries', () => {
38
beforeEach(() => {
39
jest.resetModules();
40
PropTypes = require('prop-types');
42
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
43
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
41
ReactDOM = require('react-dom');
42
React = require('react');
43
@@ -689,7 +686,7 @@ describe('ReactLegacyErrorBoundaries', () => {
686
expect(console.error.mock.calls[0][0]).toContain(
687
'ReactDOM.render is no longer supported',
688
);
692
- expect(console.error.mock.calls[1][0]).toContain(
689
+ expect(console.error.mock.calls[1][2]).toContain(
690
'The above error occurred in the <BrokenRender> component:',
691
);
692
}
packages/react-dom/src/test-utils/ReactTestUtils.js
+17
-7
@@ -21,10 +21,6 @@ import {
21
} from 'react-reconciler/src/ReactWorkTags';
22
import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
23
import {ELEMENT_NODE} from 'react-dom-bindings/src/client/HTMLNodeType';
24
-import {
25
- rethrowCaughtError,
26
- invokeGuardedCallbackAndCatchFirstError,
27
-} from 'shared/ReactErrorUtils';
24
import {enableFloat} from 'shared/ReactFeatureFlags';
25
import assign from 'shared/assign';
26
import isArray from 'shared/isArray';
@@ -354,6 +350,9 @@ function nativeTouchData(x, y) {
350
// EventPropagator.js, as they deviated from ReactDOM's newer
351
// implementations.
352
353
+let hasError: boolean = false;
354
+let caughtError: mixed = null;
355
+
356
/**
357
* Dispatch the event to the listener.
358
* @param {SyntheticEvent} event SyntheticEvent to handle
@@ -361,9 +360,15 @@ function nativeTouchData(x, y) {
360
* @param {*} inst Internal component instance
361
*/
362
function executeDispatch(event, listener, inst) {
364
- const type = event.type || 'unknown-event';
363
event.currentTarget = getNodeFromInstance(inst);
366
- invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
364
+ try {
365
+ listener(event);
366
+ } catch (error) {
367
+ if (!hasError) {
368
+ hasError = true;
369
+ caughtError = error;
370
+ }
371
+ }
372
event.currentTarget = null;
373
}
374
@@ -619,7 +624,12 @@ function makeSimulator(eventType) {
624
// do that since we're by-passing it here.
625
enqueueStateRestore(domNode);
626
executeDispatchesAndRelease(event);
622
- rethrowCaughtError();
627
+ if (hasError) {
628
+ const error = caughtError;
629
+ hasError = false;
630
+ caughtError = null;
631
+ throw error;
632
+ }
633
});
634
restoreStateIfNeeded();
635
};
packages/react-native-renderer/src/legacy-events/EventBatching.js
+1
-3
@@ -6,12 +6,10 @@
6
* @flow
7
*/
8
9
-import {rethrowCaughtError} from 'shared/ReactErrorUtils';
10
-
9
import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
10
import accumulateInto from './accumulateInto';
11
import forEachAccumulated from './forEachAccumulated';
14
-import {executeDispatchesInOrder} from './EventPluginUtils';
12
+import {executeDispatchesInOrder, rethrowCaughtError} from './EventPluginUtils';
13
14
/**
15
* Internal queue of events that have accumulated their dispatches and are
packages/react-native-renderer/src/legacy-events/EventPluginUtils.js
+22
-3
@@ -5,9 +5,11 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
8
import isArray from 'shared/isArray';
9
10
+let hasError = false;
11
+let caughtError = null;
12
+
13
export let getFiberCurrentPropsFromNode = null;
14
export let getInstanceFromNode = null;
15
export let getNodeFromInstance = null;
@@ -62,9 +64,17 @@ function validateEventDispatches(event) {
64
* @param {*} inst Internal component instance
65
*/
66
export function executeDispatch(event, listener, inst) {
65
- const type = event.type || 'unknown-event';
67
event.currentTarget = getNodeFromInstance(inst);
67
- invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
68
+ try {
69
+ listener(event);
70
+ } catch (error) {
71
+ if (!hasError) {
72
+ hasError = true;
73
+ caughtError = error;
74
+ } else {
75
+ // TODO: Make sure this error gets logged somehow.
76
+ }
77
+ }
78
event.currentTarget = null;
79
}
80
@@ -170,3 +180,12 @@ export function executeDirectDispatch(event) {
180
export function hasDispatches(event) {
181
return !!event._dispatchListeners;
182
}
183
+
184
+export function rethrowCaughtError() {
185
+ if (hasError) {
186
+ const error = caughtError;
187
+ hasError = false;
188
+ caughtError = null;
189
+ throw error;
190
+ }
191
+}
packages/react-reconciler/src/ReactFiber.js
-54
@@ -879,57 +879,3 @@ export function createFiberFromPortal(
879
};
880
return fiber;
881
}
882
-
883
-// Used for stashing WIP properties to replay failed work in DEV.
884
-export function assignFiberPropertiesInDEV(
885
- target: Fiber | null,
886
- source: Fiber,
887
-): Fiber {
888
- if (target === null) {
889
- // This Fiber's initial properties will always be overwritten.
890
- // We only use a Fiber to ensure the same hidden class so DEV isn't slow.
891
- target = createFiber(IndeterminateComponent, null, null, NoMode);
892
- }
893
-
894
- // This is intentionally written as a list of all properties.
895
- // We tried to use Object.assign() instead but this is called in
896
- // the hottest path, and Object.assign() was too slow:
897
- // https://github.com/facebook/react/issues/12502
898
- // This code is DEV-only so size is not a concern.
899
-
900
- target.tag = source.tag;
901
- target.key = source.key;
902
- target.elementType = source.elementType;
903
- target.type = source.type;
904
- target.stateNode = source.stateNode;
905
- target.return = source.return;
906
- target.child = source.child;
907
- target.sibling = source.sibling;
908
- target.index = source.index;
909
- target.ref = source.ref;
910
- target.refCleanup = source.refCleanup;
911
- target.pendingProps = source.pendingProps;
912
- target.memoizedProps = source.memoizedProps;
913
- target.updateQueue = source.updateQueue;
914
- target.memoizedState = source.memoizedState;
915
- target.dependencies = source.dependencies;
916
- target.mode = source.mode;
917
- target.flags = source.flags;
918
- target.subtreeFlags = source.subtreeFlags;
919
- target.deletions = source.deletions;
920
- target.lanes = source.lanes;
921
- target.childLanes = source.childLanes;
922
- target.alternate = source.alternate;
923
- if (enableProfilerTimer) {
924
- target.actualDuration = source.actualDuration;
925
- target.actualStartTime = source.actualStartTime;
926
- target.selfBaseDuration = source.selfBaseDuration;
927
- target.treeBaseDuration = source.treeBaseDuration;
928
- }
929
-
930
- target._debugInfo = source._debugInfo;
931
- target._debugOwner = source._debugOwner;
932
- target._debugNeedsRemount = source._debugNeedsRemount;
933
- target._debugHookTypes = source._debugHookTypes;
934
- return target;
935
-}
packages/react-reconciler/src/ReactFiberCommitWork.js
-15
@@ -190,7 +190,6 @@ import {
190
} from './ReactHookEffectTags';
191
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork';
192
import {doesFiberContain} from './ReactFiberTreeReflection';
193
-import {invokeGuardedCallback, clearCaughtError} from 'shared/ReactErrorUtils';
193
import {
194
isDevToolsPresent,
195
markComponentPassiveEffectMountStarted,
@@ -244,20 +243,6 @@ function shouldProfile(current: Fiber): boolean {
243
);
244
}
245
247
-export function reportUncaughtErrorInDEV(error: mixed) {
248
- // Wrapping each small part of the commit phase into a guarded
249
- // callback is a bit too slow (https://github.com/facebook/react/pull/21666).
250
- // But we rely on it to surface errors to DEV tools like overlays
251
- // (https://github.com/facebook/react/issues/21712).
252
- // As a compromise, rethrow only caught errors in a guard.
253
- if (__DEV__) {
254
- invokeGuardedCallback(null, () => {
255
- throw error;
256
- });
257
- clearCaughtError();
258
- }
259
-}
260
-
246
function callComponentWillUnmountWithTimer(current: Fiber, instance: any) {
247
instance.props = current.memoizedProps;
248
instance.state = current.memoizedState;
packages/react-reconciler/src/ReactFiberErrorLogger.js
+12
-27
@@ -11,7 +11,6 @@ import type {Fiber} from './ReactInternalTypes';
11
import type {CapturedValue} from './ReactCapturedValue';
12
13
import {showErrorDialog} from './ReactFiberErrorDialog';
14
-import {ClassComponent} from './ReactWorkTags';
14
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
15
import {HostRoot} from 'react-reconciler/src/ReactWorkTags';
16
@@ -33,24 +32,8 @@ export function logCapturedError(
32
const source = errorInfo.source;
33
const stack = errorInfo.stack;
34
const componentStack = stack !== null ? stack : '';
36
- // Browsers support silencing uncaught errors by calling
37
- // `preventDefault()` in window `error` handler.
38
- // We record this information as an expando on the error.
39
- if (error != null && error._suppressLogging) {
40
- if (boundary.tag === ClassComponent) {
41
- // The error is recoverable and was silenced.
42
- // Ignore it and don't print the stack addendum.
43
- // This is handy for testing error boundaries without noise.
44
- return;
45
- }
46
- // The error is fatal. Since the silencing might have
47
- // been accidental, we'll surface it anyway.
48
- // However, the browser would have silenced the original error
49
- // so we'll print it first, and then print the stack addendum.
50
- console['error'](error); // Don't transform to our wrapper
51
- // For a more detailed description of this block, see:
52
- // https://github.com/facebook/react/pull/13384
53
- }
35
+ // TODO: There's no longer a way to silence these warnings e.g. for tests.
36
+ // See https://github.com/facebook/react/pull/13384
37
38
const componentName = source ? getComponentNameFromFiber(source) : null;
39
const componentNameMessage = componentName
@@ -69,15 +52,17 @@ export function logCapturedError(
52
`React will try to recreate this component tree from scratch ` +
53
`using the error boundary you provided, ${errorBoundaryName}.`;
54
}
72
- const combinedMessage =
73
- `${componentNameMessage}\n${componentStack}\n\n` +
74
- `${errorBoundaryMessage}`;
55
76
- // In development, we provide our own message with just the component stack.
77
- // We don't include the original error message and JS stack because the browser
78
- // has already printed it. Even if the application swallows the error, it is still
79
- // displayed by the browser thanks to the DEV-only fake event trick in ReactErrorUtils.
80
- console['error'](combinedMessage); // Don't transform to our wrapper
56
+ // In development, we provide our own message which includes the component stack
57
+ // in addition to the error.
58
+ console['error'](
59
+ // Don't transform to our wrapper
60
+ '%o\n\n%s\n%s\n\n%s',
61
+ error,
62
+ componentNameMessage,
63
+ componentStack,
64
+ errorBoundaryMessage,
65
+ );
66
} else {
67
// In production, we print the error directly.
68
// This will include the message, the JS stack, and anything the browser wants to show.
packages/react-reconciler/src/ReactFiberHydrationContext.js
-7
@@ -116,13 +116,6 @@ export function markDidThrowWhileHydratingDEV() {
116
}
117
}
118
119
-export function didSuspendOrErrorWhileHydratingDEV(): boolean {
120
- if (__DEV__) {
121
- return didSuspendOrErrorDEV;
122
- }
123
- return false;
124
-}
125
-
119
function enterHydrationState(fiber: Fiber): boolean {
120
if (!supportsHydration) {
121
return false;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+3
-96
@@ -25,7 +25,6 @@ import type {OffscreenInstance} from './ReactFiberActivityComponent';
25
import type {RenderTaskFn} from './ReactFiberRootScheduler';
26
27
import {
28
- replayFailedUnitOfWorkWithInvokeGuardedCallback,
28
enableCreateEventHandleAPI,
29
enableProfilerTimer,
30
enableProfilerCommitHooks,
@@ -77,16 +76,9 @@ import {
76
preloadInstance,
77
} from './ReactFiberConfig';
78
80
-import {
81
- createWorkInProgress,
82
- assignFiberPropertiesInDEV,
83
- resetWorkInProgress,
84
-} from './ReactFiber';
79
+import {createWorkInProgress, resetWorkInProgress} from './ReactFiber';
80
import {isRootDehydrated} from './ReactFiberShellHydration';
86
-import {
87
- getIsHydrating,
88
- didSuspendOrErrorWhileHydratingDEV,
89
-} from './ReactFiberHydrationContext';
81
+import {getIsHydrating} from './ReactFiberHydrationContext';
82
import {
83
NoMode,
84
ProfileMode,
@@ -173,7 +165,7 @@ import {
165
import {requestCurrentTransition} from './ReactFiberTransition';
166
import {
167
SelectiveHydrationException,
176
- beginWork as originalBeginWork,
168
+ beginWork,
169
replayFunctionComponent,
170
} from './ReactFiberBeginWork';
171
import {completeWork} from './ReactFiberCompleteWork';
@@ -194,7 +186,6 @@ import {
186
reconnectPassiveEffects,
187
reappearLayoutEffects,
188
disconnectPassiveEffect,
197
- reportUncaughtErrorInDEV,
189
invokeLayoutEffectMountInDEV,
190
invokePassiveEffectMountInDEV,
191
invokeLayoutEffectUnmountInDEV,
@@ -237,11 +228,6 @@ import {
228
resetCurrentFiber as resetCurrentDebugFiberInDEV,
229
setCurrentFiber as setCurrentDebugFiberInDEV,
230
} from './ReactCurrentFiber';
240
-import {
241
- invokeGuardedCallback,
242
- hasCaughtError,
243
- clearCaughtError,
244
-} from 'shared/ReactErrorUtils';
231
import {
232
isDevToolsPresent,
233
markCommitStarted,
@@ -3397,7 +3383,6 @@ export function captureCommitPhaseError(
3383
error: mixed,
3384
) {
3385
if (__DEV__) {
3400
- reportUncaughtErrorInDEV(error);
3386
setIsRunningInsertionEffect(false);
3387
}
3388
if (sourceFiber.tag === HostRoot) {
@@ -3884,84 +3869,6 @@ export function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber: Fiber) {
3869
}
3870
}
3871
3887
-let beginWork;
3888
-if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
3889
- const dummyFiber = null;
3890
- beginWork = (current: null | Fiber, unitOfWork: Fiber, lanes: Lanes) => {
3891
- // If a component throws an error, we replay it again in a synchronously
3892
- // dispatched event, so that the debugger will treat it as an uncaught
3893
- // error See ReactErrorUtils for more information.
3894
-
3895
- // Before entering the begin phase, copy the work-in-progress onto a dummy
3896
- // fiber. If beginWork throws, we'll use this to reset the state.
3897
- const originalWorkInProgressCopy = assignFiberPropertiesInDEV(
3898
- dummyFiber,
3899
- unitOfWork,
3900
- );
3901
- try {
3902
- return originalBeginWork(current, unitOfWork, lanes);
3903
- } catch (originalError) {
3904
- if (
3905
- didSuspendOrErrorWhileHydratingDEV() ||
3906
- originalError === SuspenseException ||
3907
- originalError === SelectiveHydrationException ||
3908
- (originalError !== null &&
3909
- typeof originalError === 'object' &&
3910
- typeof originalError.then === 'function')
3911
- ) {
3912
- // Don't replay promises.
3913
- // Don't replay errors if we are hydrating and have already suspended or handled an error
3914
- throw originalError;
3915
- }
3916
-
3917
- // Don't reset current debug fiber, since we're about to work on the
3918
- // same fiber again.
3919
-
3920
- // Unwind the failed stack frame
3921
- resetSuspendedWorkLoopOnUnwind(unitOfWork);
3922
- unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
3923
-
3924
- // Restore the original properties of the fiber.
3925
- assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
3926
-
3927
- if (enableProfilerTimer && unitOfWork.mode & ProfileMode) {
3928
- // Reset the profiler timer.
3929
- startProfilerTimer(unitOfWork);
3930
- }
3931
-
3932
- // Run beginWork again.
3933
- invokeGuardedCallback(
3934
- null,
3935
- originalBeginWork,
3936
- null,
3937
- current,
3938
- unitOfWork,
3939
- lanes,
3940
- );
3941
-
3942
- if (hasCaughtError()) {
3943
- const replayError = clearCaughtError();
3944
- if (
3945
- typeof replayError === 'object' &&
3946
- replayError !== null &&
3947
- replayError._suppressLogging &&
3948
- typeof originalError === 'object' &&
3949
- originalError !== null &&
3950
- !originalError._suppressLogging
3951
- ) {
3952
- // If suppressed, let the flag carry over to the original error which is the one we'll rethrow.
3953
- originalError._suppressLogging = true;
3954
- }
3955
- }
3956
- // We always throw the original error in case the second render pass is not idempotent.
3957
- // This can happen if a memoized function or CommonJS module doesn't throw after first invocation.
3958
- throw originalError;
3959
- }
3960
- };
3961
-} else {
3962
- beginWork = originalBeginWork;
3963
-}
3964
-
3872
let didWarnAboutUpdateInRender = false;
3873
let didWarnAboutUpdateInRenderForAnotherComponent;
3874
if (__DEV__) {
packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
-4
@@ -3,7 +3,6 @@ describe('ErrorBoundaryReconciliation', () => {
3
let DidCatchErrorBoundary;
4
let GetDerivedErrorBoundary;
5
let React;
6
- let ReactFeatureFlags;
6
let ReactTestRenderer;
7
let span;
8
let act;
@@ -11,9 +10,6 @@ describe('ErrorBoundaryReconciliation', () => {
10
beforeEach(() => {
11
jest.resetModules();
12
14
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
15
-
16
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
13
ReactTestRenderer = require('react-test-renderer');
14
React = require('react');
15
act = require('internal-test-utils').act;
packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
-4
@@ -1,5 +1,4 @@
1
let React;
2
-let ReactFeatureFlags;
2
let ReactNoop;
3
let Scheduler;
4
let waitForAll;
@@ -12,9 +11,6 @@ let act;
11
describe('ReactBlockingMode', () => {
12
beforeEach(() => {
13
jest.resetModules();
15
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
16
-
17
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
14
React = require('react');
15
ReactNoop = require('react-noop-renderer');
16
Scheduler = require('scheduler');
packages/react-reconciler/src/__tests__/ReactConcurrentErrorRecovery-test.js
+4
-66
@@ -220,20 +220,7 @@ describe('ReactConcurrentErrorRecovery', () => {
220
221
// Because we're still suspended on A, we can't show an error boundary. We
222
// should wait for A to resolve.
223
- if (gate(flags => flags.replayFailedUnitOfWorkWithInvokeGuardedCallback)) {
224
- assertLog([
225
- 'Suspend! [A2]',
226
- 'Loading...',
227
-
228
- 'Error! [B2]',
229
- // This extra log happens when we replay the error
230
- // in invokeGuardedCallback
231
- 'Error! [B2]',
232
- 'Oops!',
233
- ]);
234
- } else {
235
- assertLog(['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']);
236
- }
223
+ assertLog(['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']);
224
// Remain on previous screen.
225
expect(root).toMatchRenderedOutput('A1B1');
226
@@ -241,25 +228,7 @@ describe('ReactConcurrentErrorRecovery', () => {
228
await act(() => {
229
resolveText('A2');
230
});
244
- if (gate(flags => flags.replayFailedUnitOfWorkWithInvokeGuardedCallback)) {
245
- assertLog([
246
- 'A2',
247
- 'Error! [B2]',
248
- // This extra log happens when we replay the error
249
- // in invokeGuardedCallback
250
- 'Error! [B2]',
251
- 'Oops!',
252
-
253
- 'A2',
254
- 'Error! [B2]',
255
- // This extra log happens when we replay the error
256
- // in invokeGuardedCallback
257
- 'Error! [B2]',
258
- 'Oops!',
259
- ]);
260
- } else {
261
- assertLog(['A2', 'Error! [B2]', 'Oops!', 'A2', 'Error! [B2]', 'Oops!']);
262
- }
231
+ assertLog(['A2', 'Error! [B2]', 'Oops!', 'A2', 'Error! [B2]', 'Oops!']);
232
// Now we can show the error boundary that's wrapped around B.
233
expect(root).toMatchRenderedOutput('A2Oops!');
234
});
@@ -323,20 +292,7 @@ describe('ReactConcurrentErrorRecovery', () => {
292
293
// Because we're still suspended on B, we can't show an error boundary. We
294
// should wait for B to resolve.
326
- if (gate(flags => flags.replayFailedUnitOfWorkWithInvokeGuardedCallback)) {
327
- assertLog([
328
- 'Error! [A2]',
329
- // This extra log happens when we replay the error
330
- // in invokeGuardedCallback
331
- 'Error! [A2]',
332
- 'Oops!',
333
-
334
- 'Suspend! [B2]',
335
- 'Loading...',
336
- ]);
337
- } else {
338
- assertLog(['Error! [A2]', 'Oops!', 'Suspend! [B2]', 'Loading...']);
339
- }
295
+ assertLog(['Error! [A2]', 'Oops!', 'Suspend! [B2]', 'Loading...']);
296
// Remain on previous screen.
297
expect(root).toMatchRenderedOutput('A1B1');
298
@@ -344,25 +300,7 @@ describe('ReactConcurrentErrorRecovery', () => {
300
await act(() => {
301
resolveText('B2');
302
});
347
- if (gate(flags => flags.replayFailedUnitOfWorkWithInvokeGuardedCallback)) {
348
- assertLog([
349
- 'Error! [A2]',
350
- // This extra log happens when we replay the error
351
- // in invokeGuardedCallback
352
- 'Error! [A2]',
353
- 'Oops!',
354
- 'B2',
355
-
356
- 'Error! [A2]',
357
- // This extra log happens when we replay the error
358
- // in invokeGuardedCallback
359
- 'Error! [A2]',
360
- 'Oops!',
361
- 'B2',
362
- ]);
363
- } else {
364
- assertLog(['Error! [A2]', 'Oops!', 'B2', 'Error! [A2]', 'Oops!', 'B2']);
365
- }
303
+ assertLog(['Error! [A2]', 'Oops!', 'B2', 'Error! [A2]', 'Oops!', 'B2']);
304
// Now we can show the error boundary that's wrapped around B.
305
expect(root).toMatchRenderedOutput('Oops!B2');
306
});
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
-7
@@ -1042,7 +1042,6 @@ describe('ReactHooks', () => {
1042
'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
1043
);
1044
}).toErrorDev([
1045
- 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1045
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1046
'Warning: React has detected a change in the order of Hooks called by App. ' +
1047
'This will lead to bugs and errors if not fixed. For more information, ' +
@@ -1105,9 +1104,6 @@ describe('ReactHooks', () => {
1104
</Boundary>,
1105
);
1106
}).toErrorDev([
1108
- // We see it twice due to replay
1109
- 'Context can only be read while React is rendering',
1110
- 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1107
'Context can only be read while React is rendering',
1108
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1109
]);
@@ -1143,9 +1139,6 @@ describe('ReactHooks', () => {
1139
</Boundary>,
1140
);
1141
}).toErrorDev([
1146
- // We see it twice due to replay
1147
- 'Context can only be read while React is rendering',
1148
- 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1142
'Context can only be read while React is rendering',
1143
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1144
]);
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+3
-5
@@ -10,7 +10,6 @@
10
11
'use strict';
12
13
-let ReactFeatureFlags = require('shared/ReactFeatureFlags');
13
let PropTypes;
14
let React;
15
let ReactNoop;
@@ -24,8 +23,6 @@ let waitForThrow;
23
describe('ReactIncrementalErrorHandling', () => {
24
beforeEach(() => {
25
jest.resetModules();
27
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
28
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
26
PropTypes = require('prop-types');
27
React = require('react');
28
ReactNoop = require('react-noop-renderer');
@@ -1513,7 +1510,8 @@ describe('ReactIncrementalErrorHandling', () => {
1510
1511
if (__DEV__) {
1512
expect(console.error).toHaveBeenCalledTimes(1);
1516
- expect(console.error.mock.calls[0][0]).toContain(
1513
+ expect(console.error.mock.calls[0][1]).toBe(notAnError);
1514
+ expect(console.error.mock.calls[0][2]).toContain(
1515
'The above error occurred in the <BadRender> component:',
1516
);
1517
} else {
@@ -1914,7 +1912,7 @@ describe('ReactIncrementalErrorHandling', () => {
1912
expect(console.error.mock.calls[0][0]).toContain(
1913
'Cannot update a component (`%s`) while rendering a different component',
1914
);
1917
- expect(console.error.mock.calls[1][0]).toContain(
1915
+ expect(console.error.mock.calls[1][2]).toContain(
1916
'The above error occurred in the <App> component',
1917
);
1918
}
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js
+110
-65
@@ -60,22 +60,34 @@ describe('ReactIncrementalErrorLogging', () => {
60
);
61
await waitForThrow('constructor error');
62
expect(console.error).toHaveBeenCalledTimes(1);
63
- expect(console.error).toHaveBeenCalledWith(
64
- __DEV__
65
- ? expect.stringMatching(
66
- new RegExp(
67
- 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
68
- '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
69
- '\\s+(in|at) span(.*)\n' +
70
- '\\s+(in|at) div(.*)\n\n' +
71
- 'Consider adding an error boundary to your tree ' +
72
- 'to customize error handling behavior\\.',
73
- ),
74
- )
75
- : expect.objectContaining({
76
- message: 'constructor error',
77
- }),
78
- );
63
+ if (__DEV__) {
64
+ expect(console.error).toHaveBeenCalledWith(
65
+ expect.stringContaining('%o'),
66
+ expect.objectContaining({
67
+ message: 'constructor error',
68
+ }),
69
+ expect.stringContaining(
70
+ 'The above error occurred in the <ErrorThrowingComponent> component:',
71
+ ),
72
+ expect.stringMatching(
73
+ new RegExp(
74
+ '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
75
+ '\\s+(in|at) span(.*)\n' +
76
+ '\\s+(in|at) div(.*)',
77
+ ),
78
+ ),
79
+ expect.stringContaining(
80
+ 'Consider adding an error boundary to your tree ' +
81
+ 'to customize error handling behavior.',
82
+ ),
83
+ );
84
+ } else {
85
+ expect(console.error).toHaveBeenCalledWith(
86
+ expect.objectContaining({
87
+ message: 'constructor error',
88
+ }),
89
+ );
90
+ }
91
});
92
93
it('should log errors that occur during the commit phase', async () => {
@@ -96,22 +108,34 @@ describe('ReactIncrementalErrorLogging', () => {
108
);
109
await waitForThrow('componentDidMount error');
110
expect(console.error).toHaveBeenCalledTimes(1);
99
- expect(console.error).toHaveBeenCalledWith(
100
- __DEV__
101
- ? expect.stringMatching(
102
- new RegExp(
103
- 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
104
- '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
105
- '\\s+(in|at) span(.*)\n' +
106
- '\\s+(in|at) div(.*)\n\n' +
107
- 'Consider adding an error boundary to your tree ' +
108
- 'to customize error handling behavior\\.',
109
- ),
110
- )
111
- : expect.objectContaining({
112
- message: 'componentDidMount error',
113
- }),
114
- );
111
+ if (__DEV__) {
112
+ expect(console.error).toHaveBeenCalledWith(
113
+ expect.stringContaining('%o'),
114
+ expect.objectContaining({
115
+ message: 'componentDidMount error',
116
+ }),
117
+ expect.stringContaining(
118
+ 'The above error occurred in the <ErrorThrowingComponent> component:',
119
+ ),
120
+ expect.stringMatching(
121
+ new RegExp(
122
+ '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
123
+ '\\s+(in|at) span(.*)\n' +
124
+ '\\s+(in|at) div(.*)',
125
+ ),
126
+ ),
127
+ expect.stringContaining(
128
+ 'Consider adding an error boundary to your tree ' +
129
+ 'to customize error handling behavior.',
130
+ ),
131
+ );
132
+ } else {
133
+ expect(console.error).toHaveBeenCalledWith(
134
+ expect.objectContaining({
135
+ message: 'componentDidMount error',
136
+ }),
137
+ );
138
+ }
139
});
140
141
it('should ignore errors thrown in log method to prevent cycle', async () => {
@@ -135,22 +159,34 @@ describe('ReactIncrementalErrorLogging', () => {
159
);
160
await waitForThrow('render error');
161
expect(logCapturedErrorCalls.length).toBe(1);
138
- expect(logCapturedErrorCalls[0]).toEqual(
139
- __DEV__
140
- ? expect.stringMatching(
141
- new RegExp(
142
- 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
143
- '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
144
- '\\s+(in|at) span(.*)\n' +
145
- '\\s+(in|at) div(.*)\n\n' +
146
- 'Consider adding an error boundary to your tree ' +
147
- 'to customize error handling behavior\\.',
148
- ),
149
- )
150
- : expect.objectContaining({
151
- message: 'render error',
152
- }),
153
- );
162
+ if (__DEV__) {
163
+ expect(console.error).toHaveBeenCalledWith(
164
+ expect.stringContaining('%o'),
165
+ expect.objectContaining({
166
+ message: 'render error',
167
+ }),
168
+ expect.stringContaining(
169
+ 'The above error occurred in the <ErrorThrowingComponent> component:',
170
+ ),
171
+ expect.stringMatching(
172
+ new RegExp(
173
+ '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
174
+ '\\s+(in|at) span(.*)\n' +
175
+ '\\s+(in|at) div(.*)',
176
+ ),
177
+ ),
178
+ expect.stringContaining(
179
+ 'Consider adding an error boundary to your tree ' +
180
+ 'to customize error handling behavior.',
181
+ ),
182
+ );
183
+ } else {
184
+ expect(logCapturedErrorCalls[0]).toEqual(
185
+ expect.objectContaining({
186
+ message: 'render error',
187
+ }),
188
+ );
189
+ }
190
// The error thrown in logCapturedError should be rethrown with a clean stack
191
expect(() => {
192
jest.runAllTimers();
@@ -194,31 +230,40 @@ describe('ReactIncrementalErrorLogging', () => {
230
'render: 0',
231
232
'render: 1',
197
- __DEV__ && 'render: 1', // replay due to invokeGuardedCallback
233
234
// Retry one more time before handling error
235
'render: 1',
201
- __DEV__ && 'render: 1', // replay due to invokeGuardedCallback
236
237
'componentWillUnmount: 0',
238
].filter(Boolean),
239
);
240
241
expect(console.error).toHaveBeenCalledTimes(1);
208
- expect(console.error).toHaveBeenCalledWith(
209
- __DEV__
210
- ? expect.stringMatching(
211
- new RegExp(
212
- 'The above error occurred in the <Foo> component:\n' +
213
- '\\s+(in|at) Foo (.*)\n' +
214
- '\\s+(in|at) ErrorBoundary (.*)\n\n' +
215
- 'React will try to recreate this component tree from scratch ' +
216
- 'using the error boundary you provided, ErrorBoundary.',
217
- ),
218
- )
219
- : expect.objectContaining({
220
- message: 'oops',
221
- }),
222
- );
242
+ if (__DEV__) {
243
+ expect(console.error).toHaveBeenCalledWith(
244
+ expect.stringContaining('%o'),
245
+ expect.objectContaining({
246
+ message: 'oops',
247
+ }),
248
+ expect.stringContaining(
249
+ 'The above error occurred in the <Foo> component:',
250
+ ),
251
+ expect.stringMatching(
252
+ new RegExp(
253
+ '\\s+(in|at) Foo (.*)\n' + '\\s+(in|at) ErrorBoundary(.*)',
254
+ ),
255
+ ),
256
+ expect.stringContaining(
257
+ 'React will try to recreate this component tree from scratch ' +
258
+ 'using the error boundary you provided, ErrorBoundary.',
259
+ ),
260
+ );
261
+ } else {
262
+ expect(console.error).toHaveBeenCalledWith(
263
+ expect.objectContaining({
264
+ message: 'oops',
265
+ }),
266
+ );
267
+ }
268
});
269
});
packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.internal.js
deleted
-43
@@ -1,43 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
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
- * @jest-environment node
8
- */
9
-
10
-'use strict';
11
-
12
-describe('ReactIncrementalErrorReplay-test', () => {
13
- const React = require('react');
14
- const ReactTestRenderer = require('react-test-renderer');
15
-
16
- it('copies all keys when stashing potentially failing work', () => {
17
- // Note: this test is fragile and relies on internals.
18
- // We almost always try to avoid such tests, but here the cost of
19
- // the list getting out of sync (and causing subtle bugs in rare cases)
20
- // is higher than the cost of maintaining the test.
21
-
22
- // This is the method we're going to test.
23
- // If this is no longer used, you can delete this test file.;
24
- const {assignFiberPropertiesInDEV} = require('../ReactFiber');
25
-
26
- // Get a real fiber.
27
- const realFiber = ReactTestRenderer.create(<div />).root._currentFiber();
28
- const stash = assignFiberPropertiesInDEV(null, realFiber);
29
-
30
- // Verify we get all the same fields.
31
- expect(realFiber).toEqual(stash);
32
-
33
- // Mutate the original.
34
- for (const key in realFiber) {
35
- realFiber[key] = key + '_' + Math.random();
36
- }
37
- expect(realFiber).not.toEqual(stash);
38
-
39
- // Verify we can still "revert" to the stashed properties.
40
- expect(assignFiberPropertiesInDEV(realFiber, stash)).toBe(realFiber);
41
- expect(realFiber).toEqual(stash);
42
- });
43
-});
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
-4
@@ -1,7 +1,6 @@
1
let React;
2
let ReactTestRenderer;
3
let Scheduler;
4
-let ReactFeatureFlags;
4
let Suspense;
5
let lazy;
6
let waitFor;
@@ -24,9 +23,6 @@ function normalizeCodeLocInfo(str) {
23
describe('ReactLazy', () => {
24
beforeEach(() => {
25
jest.resetModules();
27
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
28
-
29
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
26
React = require('react');
27
Suspense = React.Suspense;
28
lazy = React.lazy;
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
-4
@@ -1,7 +1,6 @@
1
let React;
2
let ReactDOMClient;
3
let ReactDOM;
4
-let ReactFeatureFlags;
4
let Scheduler;
5
let Suspense;
6
let act;
@@ -16,9 +15,6 @@ let waitFor;
15
describe('ReactSuspense', () => {
16
beforeEach(() => {
17
jest.resetModules();
19
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
20
-
21
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18
React = require('react');
19
ReactDOM = require('react-dom');
20
ReactDOMClient = require('react-dom/client');
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
+3
-6
@@ -1980,11 +1980,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
1980
});
1981
1982
// @gate enableLegacyCache
1983
- // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
1983
it('are properly handled for layout effect creation', async () => {
1984
let useLayoutEffectShouldThrow = false;
1985
1987
- function ThrowsInLayoutEffect() {
1986
+ function ThrowsInLayoutEffect({unused}) {
1987
Scheduler.log('ThrowsInLayoutEffect render');
1988
React.useLayoutEffect(() => {
1989
Scheduler.log('ThrowsInLayoutEffect useLayoutEffect create');
@@ -2117,9 +2116,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
2116
});
2117
2118
// @gate enableLegacyCache
2120
- // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
2119
it('are properly handled for layout effect destruction', async () => {
2122
- function ThrowsInLayoutEffectDestroy() {
2120
+ function ThrowsInLayoutEffectDestroy({unused}) {
2121
Scheduler.log('ThrowsInLayoutEffectDestroy render');
2122
React.useLayoutEffect(() => {
2123
Scheduler.log('ThrowsInLayoutEffectDestroy useLayoutEffect create');
@@ -3013,11 +3011,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
3011
3012
describe('that throw errors', () => {
3013
// @gate enableLegacyCache
3016
- // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
3014
it('are properly handled in ref callbacks', async () => {
3015
let useRefCallbackShouldThrow = false;
3016
3020
- function ThrowsInRefCallback() {
3017
+ function ThrowsInRefCallback({unused}) {
3018
Scheduler.log('ThrowsInRefCallback render');
3019
const refCallback = React.useCallback(value => {
3020
Scheduler.log('ThrowsInRefCallback refCallback ref? ' + !!value);
packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
-4
@@ -3,7 +3,6 @@ let Suspense;
3
let ReactNoop;
4
let Scheduler;
5
let act;
6
-let ReactFeatureFlags;
6
let Random;
7
8
const SEED = process.env.FUZZ_TEST_SEED || 'default';
@@ -21,9 +20,6 @@ function prettyFormat(thing) {
20
describe('ReactSuspenseFuzz', () => {
21
beforeEach(() => {
22
jest.resetModules();
24
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
25
-
26
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23
React = require('react');
24
Suspense = React.Suspense;
25
ReactNoop = require('react-noop-renderer');
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
-1
@@ -28,7 +28,6 @@ describe('ReactSuspensePlaceholder', () => {
28
ReactFeatureFlags = require('shared/ReactFeatureFlags');
29
30
ReactFeatureFlags.enableProfilerTimer = true;
31
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
31
React = require('react');
32
ReactNoop = require('react-noop-renderer');
33
Scheduler = require('scheduler');
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
-4
@@ -398,8 +398,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
398
});
399
400
// Second condition is redundant but guarantees that the test runs in prod.
401
- // TODO: Delete this feature flag.
402
- // @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
401
// @gate enableLegacyCache
402
it('retries on error', async () => {
403
class ErrorBoundary extends React.Component {
@@ -458,8 +456,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
456
});
457
458
// Second condition is redundant but guarantees that the test runs in prod.
461
- // TODO: Delete this feature flag.
462
- // @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
459
// @gate enableLegacyCache
460
it('retries on error after falling back to a placeholder', async () => {
461
class ErrorBoundary extends React.Component {
packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js
-1
@@ -11,7 +11,6 @@
11
'use strict';
12
13
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
14
-ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
14
const React = require('react');
15
const ReactTestRenderer = require('react-test-renderer');
16
const {format: prettyFormat} = require('pretty-format');
packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
+1
-3
@@ -55,9 +55,7 @@ describe 'ReactCoffeeScriptClass', ->
55
root.render React.createElement(Foo)
56
).toThrow()
57
).toErrorDev([
58
- # A failed component renders four times in DEV in concurrent mode
59
- 'No `render` method found on the Foo instance',
60
- 'No `render` method found on the Foo instance',
58
+ # A failed component renders twice in DEV in concurrent mode
59
'No `render` method found on the Foo instance',
60
'No `render` method found on the Foo instance',
61
])
packages/react/src/__tests__/ReactES6Class-test.js
+1
-5
@@ -63,11 +63,7 @@ describe('ReactES6Class', () => {
63
expect(() => {
64
expect(() => ReactDOM.flushSync(() => root.render(<Foo />))).toThrow();
65
}).toErrorDev([
66
- // A failed component renders four times in DEV in concurrent mode
67
- 'Warning: No `render` method found on the Foo instance: ' +
68
- 'you may have forgotten to define `render`.',
69
- 'Warning: No `render` method found on the Foo instance: ' +
70
- 'you may have forgotten to define `render`.',
66
+ // A failed component renders twice in DEV in concurrent mode
67
'Warning: No `render` method found on the Foo instance: ' +
68
'you may have forgotten to define `render`.',
69
'Warning: No `render` method found on the Foo instance: ' +
packages/react/src/__tests__/ReactElementValidator-test.internal.js
-4
@@ -19,16 +19,12 @@ let React;
19
let ReactDOMClient;
20
let act;
21
22
-let ReactFeatureFlags = require('shared/ReactFeatureFlags');
23
-
22
describe('ReactElementValidator', () => {
23
let ComponentClass;
24
25
beforeEach(() => {
26
jest.resetModules();
27
30
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
31
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
28
React = require('react');
29
ReactDOMClient = require('react-dom/client');
30
act = require('internal-test-utils').act;
packages/react/src/__tests__/ReactProfiler-test.internal.js
+155
-193
@@ -25,7 +25,6 @@ function loadModules({
25
enableProfilerTimer = true,
26
enableProfilerCommitHooks = true,
27
enableProfilerNestedUpdatePhase = true,
28
- replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
28
} = {}) {
29
ReactFeatureFlags = require('shared/ReactFeatureFlags');
30
@@ -33,8 +32,6 @@ function loadModules({
32
ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks;
33
ReactFeatureFlags.enableProfilerNestedUpdatePhase =
34
enableProfilerNestedUpdatePhase;
36
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback =
37
- replayFailedUnitOfWorkWithInvokeGuardedCallback;
35
36
React = require('react');
37
Scheduler = require('scheduler');
@@ -1015,205 +1012,170 @@ describe(`onRender`, () => {
1012
expect(call[5]).toBe(380); // commit time
1013
});
1014
1018
- [true, false].forEach(replayFailedUnitOfWorkWithInvokeGuardedCallback => {
1019
- describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
1020
- replayFailedUnitOfWorkWithInvokeGuardedCallback ? 'enabled' : 'disabled'
1021
- }`, () => {
1022
- beforeEach(() => {
1023
- jest.resetModules();
1024
-
1025
- loadModules({
1026
- replayFailedUnitOfWorkWithInvokeGuardedCallback,
1027
- });
1028
- });
1029
-
1030
- it('should accumulate actual time after an error handled by componentDidCatch()', async () => {
1031
- const callback = jest.fn();
1032
-
1033
- const ThrowsError = ({unused}) => {
1034
- Scheduler.unstable_advanceTime(3);
1035
- throw Error('expected error');
1036
- };
1037
-
1038
- class ErrorBoundary extends React.Component {
1039
- state = {error: null};
1040
- componentDidCatch(error) {
1041
- this.setState({error});
1042
- }
1043
- render() {
1044
- Scheduler.unstable_advanceTime(2);
1045
- return this.state.error === null ? (
1046
- this.props.children
1047
- ) : (
1048
- <AdvanceTime byAmount={20} />
1049
- );
1050
- }
1051
- }
1015
+ it('should accumulate actual time after an error handled by componentDidCatch()', async () => {
1016
+ const callback = jest.fn();
1017
1053
- Scheduler.unstable_advanceTime(5); // 0 -> 5
1054
-
1055
- await act(() => {
1056
- ReactNoop.render(
1057
- <React.Profiler id="test" onRender={callback}>
1058
- <ErrorBoundary>
1059
- <AdvanceTime byAmount={9} />
1060
- <ThrowsError />
1061
- </ErrorBoundary>
1062
- </React.Profiler>,
1063
- );
1064
- });
1065
-
1066
- expect(callback).toHaveBeenCalledTimes(2);
1067
-
1068
- // Callbacks bubble (reverse order).
1069
- const [mountCall, updateCall] = callback.mock.calls;
1070
-
1071
- // The initial mount only includes the ErrorBoundary (which takes 2)
1072
- // But it spends time rendering all of the failed subtree also.
1073
- expect(mountCall[1]).toBe('mount');
1074
- // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
1075
- // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1076
- expect(mountCall[2]).toBe(14);
1077
- // base time includes: 2 (ErrorBoundary)
1078
- // Since the tree is empty for the initial commit
1079
- expect(mountCall[3]).toBe(2);
1080
- // start time: 5 initially + 14 of work
1081
- // Add an additional 3 (ThrowsError) if we replayed the failed work
1082
- expect(mountCall[4]).toBe(
1083
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1084
- ? 22
1085
- : 19,
1086
- );
1087
- // commit time: 19 initially + 14 of work
1088
- // Add an additional 6 (ThrowsError *2) if we replayed the failed work
1089
- expect(mountCall[5]).toBe(
1090
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1091
- ? 39
1092
- : 33,
1093
- );
1018
+ const ThrowsError = ({unused}) => {
1019
+ Scheduler.unstable_advanceTime(3);
1020
+ throw Error('expected error');
1021
+ };
1022
1095
- // The update includes the ErrorBoundary and its fallback child
1096
- expect(updateCall[1]).toBe('nested-update');
1097
- // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1098
- expect(updateCall[2]).toBe(22);
1099
- // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1100
- expect(updateCall[3]).toBe(22);
1101
- // start time
1102
- expect(updateCall[4]).toBe(
1103
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1104
- ? 39
1105
- : 33,
1106
- );
1107
- // commit time: 33 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
1108
- // Add an additional 6 (ThrowsError *2) if we replayed the failed work
1109
- expect(updateCall[5]).toBe(
1110
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1111
- ? 61
1112
- : 55,
1023
+ class ErrorBoundary extends React.Component {
1024
+ state = {error: null};
1025
+ componentDidCatch(error) {
1026
+ this.setState({error});
1027
+ }
1028
+ render() {
1029
+ Scheduler.unstable_advanceTime(2);
1030
+ return this.state.error === null ? (
1031
+ this.props.children
1032
+ ) : (
1033
+ <AdvanceTime byAmount={20} />
1034
);
1114
- });
1115
-
1116
- it('should accumulate actual time after an error handled by getDerivedStateFromError()', async () => {
1117
- const callback = jest.fn();
1118
-
1119
- const ThrowsError = ({unused}) => {
1120
- Scheduler.unstable_advanceTime(10);
1121
- throw Error('expected error');
1122
- };
1123
-
1124
- class ErrorBoundary extends React.Component {
1125
- state = {error: null};
1126
- static getDerivedStateFromError(error) {
1127
- return {error};
1128
- }
1129
- render() {
1130
- Scheduler.unstable_advanceTime(2);
1131
- return this.state.error === null ? (
1132
- this.props.children
1133
- ) : (
1134
- <AdvanceTime byAmount={20} />
1135
- );
1136
- }
1137
- }
1035
+ }
1036
+ }
1037
1139
- Scheduler.unstable_advanceTime(5); // 0 -> 5
1140
-
1141
- await act(() => {
1142
- ReactNoop.render(
1143
- <React.Profiler id="test" onRender={callback}>
1144
- <ErrorBoundary>
1145
- <AdvanceTime byAmount={5} />
1146
- <ThrowsError />
1147
- </ErrorBoundary>
1148
- </React.Profiler>,
1149
- );
1150
- });
1151
-
1152
- expect(callback).toHaveBeenCalledTimes(1);
1153
-
1154
- // Callbacks bubble (reverse order).
1155
- const [mountCall] = callback.mock.calls;
1156
-
1157
- // The initial mount includes the ErrorBoundary's error state,
1158
- // But it also spends actual time rendering UI that fails and isn't included.
1159
- expect(mountCall[1]).toBe('mount');
1160
- // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
1161
- // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
1162
- // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1163
- expect(mountCall[2]).toBe(39);
1164
- // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1165
- expect(mountCall[3]).toBe(22);
1166
- // start time
1167
- expect(mountCall[4]).toBe(
1168
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1169
- ? 54
1170
- : 44,
1171
- );
1172
- // commit time
1173
- expect(mountCall[5]).toBe(
1174
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1175
- ? 103
1176
- : 83,
1177
- );
1178
- });
1038
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
1039
+
1040
+ const root = ReactNoop.createRoot();
1041
+ await act(() => {
1042
+ root.render(
1043
+ <React.Profiler id="test" onRender={callback}>
1044
+ <ErrorBoundary>
1045
+ <AdvanceTime byAmount={9} />
1046
+ <ThrowsError />
1047
+ </ErrorBoundary>
1048
+ </React.Profiler>,
1049
+ );
1050
+ });
1051
1180
- it('should reset the fiber stack correct after a "complete" phase error', async () => {
1181
- jest.resetModules();
1052
+ expect(callback).toHaveBeenCalledTimes(2);
1053
1183
- loadModules({
1184
- replayFailedUnitOfWorkWithInvokeGuardedCallback,
1185
- });
1054
+ // Callbacks bubble (reverse order).
1055
+ const [mountCall, updateCall] = callback.mock.calls;
1056
+
1057
+ // The initial mount only includes the ErrorBoundary (which takes 2)
1058
+ // But it spends time rendering all of the failed subtree also.
1059
+ expect(mountCall[1]).toBe('mount');
1060
+ // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
1061
+ // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1062
+ expect(mountCall[2]).toBe(14);
1063
+ // base time includes: 2 (ErrorBoundary)
1064
+ // Since the tree is empty for the initial commit
1065
+ expect(mountCall[3]).toBe(2);
1066
+
1067
+ // start time: 5 initially + 14 of work
1068
+ // Add an additional 3 (ThrowsError) if we replayed the failed work
1069
+ expect(mountCall[4]).toBe(19);
1070
+ // commit time: 19 initially + 14 of work
1071
+ // Add an additional 6 (ThrowsError *2) if we replayed the failed work
1072
+ expect(mountCall[5]).toBe(33);
1073
+
1074
+ // The update includes the ErrorBoundary and its fallback child
1075
+ expect(updateCall[1]).toBe('nested-update');
1076
+ // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1077
+ expect(updateCall[2]).toBe(22);
1078
+ // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1079
+ expect(updateCall[3]).toBe(22);
1080
+ // start time
1081
+ expect(updateCall[4]).toBe(33);
1082
+ // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
1083
+ // Add an additional 3 (ThrowsError) if we replayed the failed work
1084
+ expect(updateCall[5]).toBe(55);
1085
+ });
1086
1187
- // Simulate a renderer error during the "complete" phase.
1188
- // This mimics behavior like React Native's View/Text nesting validation.
1189
- ReactNoop.render(
1190
- <React.Profiler id="profiler" onRender={jest.fn()}>
1191
- <errorInCompletePhase>hi</errorInCompletePhase>
1192
- </React.Profiler>,
1193
- );
1194
- await waitForThrow('Error in host config.');
1195
-
1196
- // A similar case we've seen caused by an invariant in ReactDOM.
1197
- // It didn't reproduce without a host component inside.
1198
- ReactNoop.render(
1199
- <React.Profiler id="profiler" onRender={jest.fn()}>
1200
- <errorInCompletePhase>
1201
- <span>hi</span>
1202
- </errorInCompletePhase>
1203
- </React.Profiler>,
1204
- );
1205
- await waitForThrow('Error in host config.');
1206
-
1207
- // So long as the profiler timer's fiber stack is reset correctly,
1208
- // Subsequent renders should not error.
1209
- ReactNoop.render(
1210
- <React.Profiler id="profiler" onRender={jest.fn()}>
1211
- <span>hi</span>
1212
- </React.Profiler>,
1087
+ it('should accumulate actual time after an error handled by getDerivedStateFromError()', async () => {
1088
+ const callback = jest.fn();
1089
+
1090
+ const ThrowsError = ({unused}) => {
1091
+ Scheduler.unstable_advanceTime(10);
1092
+ throw Error('expected error');
1093
+ };
1094
+
1095
+ class ErrorBoundary extends React.Component {
1096
+ state = {error: null};
1097
+ static getDerivedStateFromError(error) {
1098
+ return {error};
1099
+ }
1100
+ render() {
1101
+ Scheduler.unstable_advanceTime(2);
1102
+ return this.state.error === null ? (
1103
+ this.props.children
1104
+ ) : (
1105
+ <AdvanceTime byAmount={20} />
1106
);
1214
- await waitForAll([]);
1215
- });
1107
+ }
1108
+ }
1109
+
1110
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
1111
+
1112
+ await act(() => {
1113
+ const root = ReactNoop.createRoot();
1114
+ root.render(
1115
+ <React.Profiler id="test" onRender={callback}>
1116
+ <ErrorBoundary>
1117
+ <AdvanceTime byAmount={5} />
1118
+ <ThrowsError />
1119
+ </ErrorBoundary>
1120
+ </React.Profiler>,
1121
+ );
1122
});
1123
+
1124
+ expect(callback).toHaveBeenCalledTimes(1);
1125
+
1126
+ // Callbacks bubble (reverse order).
1127
+ const [mountCall] = callback.mock.calls;
1128
+
1129
+ // The initial mount includes the ErrorBoundary's error state,
1130
+ // But it also spends actual time rendering UI that fails and isn't included.
1131
+ expect(mountCall[1]).toBe('mount');
1132
+ // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
1133
+ // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
1134
+ // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1135
+ expect(mountCall[2]).toBe(39);
1136
+ // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1137
+ expect(mountCall[3]).toBe(22);
1138
+ // start time
1139
+ expect(mountCall[4]).toBe(44);
1140
+ // commit time
1141
+ expect(mountCall[5]).toBe(83);
1142
+ });
1143
+
1144
+ it('should reset the fiber stack correct after a "complete" phase error', async () => {
1145
+ jest.resetModules();
1146
+
1147
+ loadModules({
1148
+ useNoopRenderer: true,
1149
+ });
1150
+
1151
+ // Simulate a renderer error during the "complete" phase.
1152
+ // This mimics behavior like React Native's View/Text nesting validation.
1153
+ ReactNoop.render(
1154
+ <React.Profiler id="profiler" onRender={jest.fn()}>
1155
+ <errorInCompletePhase>hi</errorInCompletePhase>
1156
+ </React.Profiler>,
1157
+ );
1158
+ await waitForThrow('Error in host config.');
1159
+
1160
+ // A similar case we've seen caused by an invariant in ReactDOM.
1161
+ // It didn't reproduce without a host component inside.
1162
+ ReactNoop.render(
1163
+ <React.Profiler id="profiler" onRender={jest.fn()}>
1164
+ <errorInCompletePhase>
1165
+ <span>hi</span>
1166
+ </errorInCompletePhase>
1167
+ </React.Profiler>,
1168
+ );
1169
+ await waitForThrow('Error in host config.');
1170
+
1171
+ // So long as the profiler timer's fiber stack is reset correctly,
1172
+ // Subsequent renders should not error.
1173
+ ReactNoop.render(
1174
+ <React.Profiler id="profiler" onRender={jest.fn()}>
1175
+ <span>hi</span>
1176
+ </React.Profiler>,
1177
+ );
1178
+ await waitForAll([]);
1179
});
1180
});
1181
packages/react/src/__tests__/ReactTypeScriptClass-test.ts
+1
-5
@@ -332,11 +332,7 @@ describe('ReactTypeScriptClass', function() {
332
ReactDOM.flushSync(() => root.render(React.createElement(Empty)))
333
).toThrow();
334
}).toErrorDev([
335
- // A failed component renders four times in DEV in concurrent mode
336
- 'Warning: No `render` method found on the Empty instance: ' +
337
- 'you may have forgotten to define `render`.',
338
- 'Warning: No `render` method found on the Empty instance: ' +
339
- 'you may have forgotten to define `render`.',
335
+ // A failed component renders twice in DEV in concurrent mode
336
'Warning: No `render` method found on the Empty instance: ' +
337
'you may have forgotten to define `render`.',
338
'Warning: No `render` method found on the Empty instance: ' +
packages/react/src/__tests__/forwardRef-test.internal.js
-3
@@ -11,16 +11,13 @@
11
12
describe('forwardRef', () => {
13
let React;
14
- let ReactFeatureFlags;
14
let ReactNoop;
15
let Scheduler;
16
let waitForAll;
17
18
beforeEach(() => {
19
jest.resetModules();
21
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
20
23
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
21
React = require('react');
22
ReactNoop = require('react-noop-renderer');
23
Scheduler = require('scheduler');
packages/shared/ReactErrorUtils.js
deleted
-125
@@ -1,125 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
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 invokeGuardedCallbackImpl from './invokeGuardedCallbackImpl';
11
-
12
-// Used by Fiber to simulate a try-catch.
13
-let hasError: boolean = false;
14
-let caughtError: mixed = null;
15
-
16
-// Used by event system to capture/rethrow the first error.
17
-let hasRethrowError: boolean = false;
18
-let rethrowError: mixed = null;
19
-
20
-const reporter = {
21
- onError(error: mixed) {
22
- hasError = true;
23
- caughtError = error;
24
- },
25
-};
26
-
27
-/**
28
- * Call a function while guarding against errors that happens within it.
29
- * Returns an error if it throws, otherwise null.
30
- *
31
- * In production, this is implemented using a try-catch. The reason we don't
32
- * use a try-catch directly is so that we can swap out a different
33
- * implementation in DEV mode.
34
- *
35
- * @param {String} name of the guard to use for logging or debugging
36
- * @param {Function} func The function to invoke
37
- * @param {*} context The context to use when calling the function
38
- * @param {...*} args Arguments for function
39
- */
40
-export function invokeGuardedCallback<A, B, C, D, E, F, Context>(
41
- name: string | null,
42
- func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
43
- context: Context,
44
- a: A,
45
- b: B,
46
- c: C,
47
- d: D,
48
- e: E,
49
- f: F,
50
-): void {
51
- hasError = false;
52
- caughtError = null;
53
- invokeGuardedCallbackImpl.apply(reporter, arguments);
54
-}
55
-
56
-/**
57
- * Same as invokeGuardedCallback, but instead of returning an error, it stores
58
- * it in a global so it can be rethrown by `rethrowCaughtError` later.
59
- * TODO: See if caughtError and rethrowError can be unified.
60
- *
61
- * @param {String} name of the guard to use for logging or debugging
62
- * @param {Function} func The function to invoke
63
- * @param {*} context The context to use when calling the function
64
- * @param {...*} args Arguments for function
65
- */
66
-export function invokeGuardedCallbackAndCatchFirstError<
67
- A,
68
- B,
69
- C,
70
- D,
71
- E,
72
- F,
73
- Context,
74
->(
75
- this: mixed,
76
- name: string | null,
77
- func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
78
- context: Context,
79
- a: A,
80
- b: B,
81
- c: C,
82
- d: D,
83
- e: E,
84
- f: F,
85
-): void {
86
- invokeGuardedCallback.apply(this, arguments);
87
- if (hasError) {
88
- const error = clearCaughtError();
89
- if (!hasRethrowError) {
90
- hasRethrowError = true;
91
- rethrowError = error;
92
- }
93
- }
94
-}
95
-
96
-/**
97
- * During execution of guarded functions we will capture the first error which
98
- * we will rethrow to be handled by the top level error handler.
99
- */
100
-export function rethrowCaughtError() {
101
- if (hasRethrowError) {
102
- const error = rethrowError;
103
- hasRethrowError = false;
104
- rethrowError = null;
105
- throw error;
106
- }
107
-}
108
-
109
-export function hasCaughtError(): boolean {
110
- return hasError;
111
-}
112
-
113
-export function clearCaughtError(): mixed {
114
- if (hasError) {
115
- const error = caughtError;
116
- hasError = false;
117
- caughtError = null;
118
- return error;
119
- } else {
120
- throw new Error(
121
- 'clearCaughtError was called but no error was captured. This error ' +
122
- 'is likely caused by a bug in React. Please file an issue.',
123
- );
124
- }
125
-}
packages/shared/ReactFeatureFlags.js
-4
@@ -251,10 +251,6 @@ export const enableSchedulingProfiler = __PROFILE__;
251
// reducers by double invoking them in StrictLegacyMode.
252
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
253
254
-// To preserve the "Pause on caught exceptions" behavior of the debugger, we
255
-// replay the begin phase of a failed component inside invokeGuardedCallback.
256
-export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
257
-
254
// Gather advanced timing metrics for Profiler subtrees.
255
export const enableProfilerTimer = __PROFILE__;
256
packages/shared/__tests__/ReactErrorUtils-test.internal.js
deleted
-210
@@ -1,210 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
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
- * @emails react-core
8
- */
9
-
10
-'use strict';
11
-
12
-let ReactErrorUtils;
13
-
14
-describe('ReactErrorUtils', () => {
15
- beforeEach(() => {
16
- // TODO: can we express this test with only public API?
17
- ReactErrorUtils = require('shared/ReactErrorUtils');
18
- });
19
-
20
- it(`it should rethrow caught errors`, () => {
21
- const err = new Error('foo');
22
- const callback = function () {
23
- throw err;
24
- };
25
- ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
26
- 'foo',
27
- callback,
28
- null,
29
- );
30
- expect(ReactErrorUtils.hasCaughtError()).toBe(false);
31
- expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
32
- });
33
-
34
- it(`should call the callback the passed arguments`, () => {
35
- const callback = jest.fn();
36
- ReactErrorUtils.invokeGuardedCallback(
37
- 'foo',
38
- callback,
39
- null,
40
- 'arg1',
41
- 'arg2',
42
- );
43
- expect(callback).toBeCalledWith('arg1', 'arg2');
44
- });
45
-
46
- it(`should call the callback with the provided context`, () => {
47
- const context = {didCall: false};
48
- ReactErrorUtils.invokeGuardedCallback(
49
- 'foo',
50
- function () {
51
- this.didCall = true;
52
- },
53
- context,
54
- );
55
- expect(context.didCall).toBe(true);
56
- });
57
-
58
- it(`should catch errors`, () => {
59
- const error = new Error();
60
- const returnValue = ReactErrorUtils.invokeGuardedCallback(
61
- 'foo',
62
- function () {
63
- throw error;
64
- },
65
- null,
66
- 'arg1',
67
- 'arg2',
68
- );
69
- expect(returnValue).toBe(undefined);
70
- expect(ReactErrorUtils.hasCaughtError()).toBe(true);
71
- expect(ReactErrorUtils.clearCaughtError()).toBe(error);
72
- });
73
-
74
- it(`should return false from clearCaughtError if no error was thrown`, () => {
75
- const callback = jest.fn();
76
- ReactErrorUtils.invokeGuardedCallback('foo', callback, null);
77
- expect(ReactErrorUtils.hasCaughtError()).toBe(false);
78
- expect(ReactErrorUtils.clearCaughtError).toThrow('no error was captured');
79
- });
80
-
81
- it(`can nest with same debug name`, () => {
82
- const err1 = new Error();
83
- let err2;
84
- const err3 = new Error();
85
- ReactErrorUtils.invokeGuardedCallback(
86
- 'foo',
87
- function () {
88
- ReactErrorUtils.invokeGuardedCallback(
89
- 'foo',
90
- function () {
91
- throw err1;
92
- },
93
- null,
94
- );
95
- err2 = ReactErrorUtils.clearCaughtError();
96
- throw err3;
97
- },
98
- null,
99
- );
100
- const err4 = ReactErrorUtils.clearCaughtError();
101
-
102
- expect(err2).toBe(err1);
103
- expect(err4).toBe(err3);
104
- });
105
-
106
- it(`handles nested errors`, () => {
107
- const err1 = new Error();
108
- let err2;
109
- ReactErrorUtils.invokeGuardedCallback(
110
- 'foo',
111
- function () {
112
- ReactErrorUtils.invokeGuardedCallback(
113
- 'foo',
114
- function () {
115
- throw err1;
116
- },
117
- null,
118
- );
119
- err2 = ReactErrorUtils.clearCaughtError();
120
- },
121
- null,
122
- );
123
- // Returns null because inner error was already captured
124
- expect(ReactErrorUtils.hasCaughtError()).toBe(false);
125
-
126
- expect(err2).toBe(err1);
127
- });
128
-
129
- it('handles nested errors in separate renderers', () => {
130
- const ReactErrorUtils1 = require('shared/ReactErrorUtils');
131
- jest.resetModules();
132
- const ReactErrorUtils2 = require('shared/ReactErrorUtils');
133
- expect(ReactErrorUtils1).not.toEqual(ReactErrorUtils2);
134
-
135
- const ops = [];
136
-
137
- ReactErrorUtils1.invokeGuardedCallback(
138
- null,
139
- () => {
140
- ReactErrorUtils2.invokeGuardedCallback(
141
- null,
142
- () => {
143
- throw new Error('nested error');
144
- },
145
- null,
146
- );
147
- // ReactErrorUtils2 should catch the error
148
- ops.push(ReactErrorUtils2.hasCaughtError());
149
- ops.push(ReactErrorUtils2.clearCaughtError().message);
150
- },
151
- null,
152
- );
153
-
154
- // ReactErrorUtils1 should not catch the error
155
- ops.push(ReactErrorUtils1.hasCaughtError());
156
-
157
- expect(ops).toEqual([true, 'nested error', false]);
158
- });
159
-
160
- if (!__DEV__) {
161
- // jsdom doesn't handle this properly, but Chrome and Firefox should. Test
162
- // this with a fixture.
163
- it('catches null values', () => {
164
- ReactErrorUtils.invokeGuardedCallback(
165
- null,
166
- function () {
167
- throw null; // eslint-disable-line no-throw-literal
168
- },
169
- null,
170
- );
171
- expect(ReactErrorUtils.hasCaughtError()).toBe(true);
172
- expect(ReactErrorUtils.clearCaughtError()).toBe(null);
173
- });
174
- }
175
-
176
- it(`can be shimmed`, () => {
177
- const ops = [];
178
- jest.resetModules();
179
- jest.mock(
180
- 'shared/invokeGuardedCallbackImpl',
181
- () =>
182
- function invokeGuardedCallback(name, func, context, a) {
183
- ops.push(a);
184
- try {
185
- func.call(context, a);
186
- } catch (error) {
187
- this.onError(error);
188
- }
189
- },
190
- );
191
- ReactErrorUtils = require('shared/ReactErrorUtils');
192
-
193
- try {
194
- const err = new Error('foo');
195
- const callback = function () {
196
- throw err;
197
- };
198
- ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
199
- 'foo',
200
- callback,
201
- null,
202
- 'somearg',
203
- );
204
- expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
205
- expect(ops).toEqual(['somearg']);
206
- } finally {
207
- jest.unmock('shared/invokeGuardedCallbackImpl');
208
- }
209
- });
210
-});
packages/shared/forks/ReactFeatureFlags.native-fb.js
-1
@@ -54,7 +54,6 @@ export const disableJavaScriptURLs = true;
54
export const disableCommentsAsDOMContainers = true;
55
export const disableInputAttributeSyncing = false;
56
export const disableIEWorkarounds = true;
57
-export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
57
export const enableScopeAPI = false;
58
export const enableCreateEventHandleAPI = false;
59
export const enableSuspenseCallback = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
-1
@@ -14,7 +14,6 @@ export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
14
export const enableDebugTracing = false;
15
export const enableAsyncDebugInfo = false;
16
export const enableSchedulingProfiler = false;
17
-export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
17
export const enableProfilerTimer = __PROFILE__;
18
export const enableProfilerCommitHooks = __PROFILE__;
19
export const enableProfilerNestedUpdatePhase = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
-1
@@ -14,7 +14,6 @@ export const debugRenderPhaseSideEffectsForStrictMode = false;
14
export const enableDebugTracing = false;
15
export const enableAsyncDebugInfo = false;
16
export const enableSchedulingProfiler = false;
17
-export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
17
export const enableProfilerTimer = __PROFILE__;
18
export const enableProfilerCommitHooks = __PROFILE__;
19
export const enableProfilerNestedUpdatePhase = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
-1
@@ -14,7 +14,6 @@ export const debugRenderPhaseSideEffectsForStrictMode = false;
14
export const enableDebugTracing = false;
15
export const enableAsyncDebugInfo = false;
16
export const enableSchedulingProfiler = false;
17
-export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
17
export const enableProfilerTimer = __PROFILE__;
18
export const enableProfilerCommitHooks = __PROFILE__;
19
export const enableProfilerNestedUpdatePhase = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
-1
@@ -14,7 +14,6 @@ export const debugRenderPhaseSideEffectsForStrictMode = false;
14
export const enableDebugTracing = false;
15
export const enableAsyncDebugInfo = false;
16
export const enableSchedulingProfiler = false;
17
-export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
17
export const enableProfilerTimer = __PROFILE__;
18
export const enableProfilerCommitHooks = __PROFILE__;
19
export const enableProfilerNestedUpdatePhase = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
-4
@@ -44,10 +44,6 @@ export const enableSchedulingProfiler = __VARIANT__;
44
45
export const enableInfiniteRenderLoopDetection = __VARIANT__;
46
47
-// These are already tested in both modes using the build type dimension,
48
-// so we don't need to use __VARIANT__ to get extra coverage.
49
-export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
50
-
47
// TODO: These flags are hard-coded to the default values used in open source.
48
// Update the tests so that they pass in either mode, then set these
49
// to __VARIANT__.
packages/shared/forks/ReactFeatureFlags.www.js
-1
@@ -18,7 +18,6 @@ export const {
18
disableInputAttributeSyncing,
19
disableIEWorkarounds,
20
enableTrustedTypesIntegration,
21
- replayFailedUnitOfWorkWithInvokeGuardedCallback,
21
enableLegacyFBSupport,
22
enableDebugTracing,
23
enableUseRefAccessWarning,
packages/shared/forks/invokeGuardedCallbackImpl.www.js
deleted
-34
@@ -1,34 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
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
- * @noflow
8
- */
9
-
10
-// Provided by www
11
-const ReactFbErrorUtils = require('ReactFbErrorUtils');
12
-
13
-if (typeof ReactFbErrorUtils.invokeGuardedCallback !== 'function') {
14
- throw new Error(
15
- 'Expected ReactFbErrorUtils.invokeGuardedCallback to be a function.',
16
- );
17
-}
18
-
19
-function invokeGuardedCallbackImpl<A, B, C, D, E, F, Context>(
20
- name: string | null,
21
- func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
22
- context: Context,
23
- a: A,
24
- b: B,
25
- c: C,
26
- d: D,
27
- e: E,
28
- f: F,
29
-) {
30
- // This will call `this.onError(err)` if an error was caught.
31
- ReactFbErrorUtils.invokeGuardedCallback.apply(this, arguments);
32
-}
33
-
34
-export default invokeGuardedCallbackImpl;
packages/shared/invokeGuardedCallbackImpl.js
deleted
-215
@@ -1,215 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
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
-let fakeNode: Element = (null: any);
11
-if (__DEV__) {
12
- if (
13
- typeof window !== 'undefined' &&
14
- typeof window.dispatchEvent === 'function' &&
15
- typeof document !== 'undefined' &&
16
- // $FlowFixMe[method-unbinding]
17
- typeof document.createEvent === 'function'
18
- ) {
19
- fakeNode = document.createElement('react');
20
- }
21
-}
22
-
23
-export default function invokeGuardedCallbackImpl<Args: Array<mixed>, Context>(
24
- this: {onError: (error: mixed) => void},
25
- name: string | null,
26
- func: (...Args) => mixed,
27
- context: Context,
28
-): void {
29
- if (__DEV__) {
30
- // In DEV mode, we use a special version
31
- // that plays more nicely with the browser's DevTools. The idea is to preserve
32
- // "Pause on exceptions" behavior. Because React wraps all user-provided
33
- // functions in invokeGuardedCallback, and the production version of
34
- // invokeGuardedCallback uses a try-catch, all user exceptions are treated
35
- // like caught exceptions, and the DevTools won't pause unless the developer
36
- // takes the extra step of enabling pause on caught exceptions. This is
37
- // unintuitive, though, because even though React has caught the error, from
38
- // the developer's perspective, the error is uncaught.
39
- //
40
- // To preserve the expected "Pause on exceptions" behavior, we don't use a
41
- // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
42
- // DOM node, and call the user-provided callback from inside an event handler
43
- // for that fake event. If the callback throws, the error is "captured" using
44
- // event loop context, it does not interrupt the normal program flow.
45
- // Effectively, this gives us try-catch behavior without actually using
46
- // try-catch. Neat!
47
-
48
- // fakeNode signifies we are in an environment with a document and window object
49
- if (fakeNode) {
50
- const evt = document.createEvent('Event');
51
-
52
- let didCall = false;
53
- // Keeps track of whether the user-provided callback threw an error. We
54
- // set this to true at the beginning, then set it to false right after
55
- // calling the function. If the function errors, `didError` will never be
56
- // set to false. This strategy works even if the browser is flaky and
57
- // fails to call our global error handler, because it doesn't rely on
58
- // the error event at all.
59
- let didError = true;
60
-
61
- // Keeps track of the value of window.event so that we can reset it
62
- // during the callback to let user code access window.event in the
63
- // browsers that support it.
64
- const windowEvent = window.event;
65
-
66
- // Keeps track of the descriptor of window.event to restore it after event
67
- // dispatching: https://github.com/facebook/react/issues/13688
68
- const windowEventDescriptor = Object.getOwnPropertyDescriptor(
69
- window,
70
- 'event',
71
- );
72
-
73
- const restoreAfterDispatch = () => {
74
- // We immediately remove the callback from event listeners so that
75
- // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
76
- // nested call would trigger the fake event handlers of any call higher
77
- // in the stack.
78
- fakeNode.removeEventListener(evtType, callCallback, false);
79
-
80
- // We check for window.hasOwnProperty('event') to prevent the
81
- // window.event assignment in both IE <= 10 as they throw an error
82
- // "Member not found" in strict mode, and in Firefox which does not
83
- // support window.event.
84
- if (
85
- typeof window.event !== 'undefined' &&
86
- window.hasOwnProperty('event')
87
- ) {
88
- window.event = windowEvent;
89
- }
90
- };
91
-
92
- // Create an event handler for our fake event. We will synchronously
93
- // dispatch our fake event using `dispatchEvent`. Inside the handler, we
94
- // call the user-provided callback.
95
- // $FlowFixMe[method-unbinding]
96
- const funcArgs = Array.prototype.slice.call(arguments, 3);
97
- const callCallback = () => {
98
- didCall = true;
99
- restoreAfterDispatch();
100
- // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
101
- func.apply(context, funcArgs);
102
- didError = false;
103
- };
104
-
105
- // Create a global error event handler. We use this to capture the value
106
- // that was thrown. It's possible that this error handler will fire more
107
- // than once; for example, if non-React code also calls `dispatchEvent`
108
- // and a handler for that event throws. We should be resilient to most of
109
- // those cases. Even if our error event handler fires more than once, the
110
- // last error event is always used. If the callback actually does error,
111
- // we know that the last error event is the correct one, because it's not
112
- // possible for anything else to have happened in between our callback
113
- // erroring and the code that follows the `dispatchEvent` call below. If
114
- // the callback doesn't error, but the error event was fired, we know to
115
- // ignore it because `didError` will be false, as described above.
116
- let error;
117
- // Use this to track whether the error event is ever called.
118
- let didSetError = false;
119
- let isCrossOriginError = false;
120
-
121
- const handleWindowError = (event: ErrorEvent) => {
122
- error = event.error;
123
- didSetError = true;
124
- if (error === null && event.colno === 0 && event.lineno === 0) {
125
- isCrossOriginError = true;
126
- }
127
- if (event.defaultPrevented) {
128
- // Some other error handler has prevented default.
129
- // Browsers silence the error report if this happens.
130
- // We'll remember this to later decide whether to log it or not.
131
- if (error != null && typeof error === 'object') {
132
- try {
133
- error._suppressLogging = true;
134
- } catch (inner) {
135
- // Ignore.
136
- }
137
- }
138
- }
139
- };
140
-
141
- // Create a fake event type.
142
- const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
143
-
144
- // Attach our event handlers
145
- window.addEventListener('error', handleWindowError);
146
- fakeNode.addEventListener(evtType, callCallback, false);
147
-
148
- // Synchronously dispatch our fake event. If the user-provided function
149
- // errors, it will trigger our global error handler.
150
- evt.initEvent(evtType, false, false);
151
- fakeNode.dispatchEvent(evt);
152
- if (windowEventDescriptor) {
153
- Object.defineProperty(window, 'event', windowEventDescriptor);
154
- }
155
-
156
- if (didCall && didError) {
157
- if (!didSetError) {
158
- // The callback errored, but the error event never fired.
159
- // eslint-disable-next-line react-internal/prod-error-codes
160
- error = new Error(
161
- 'An error was thrown inside one of your components, but React ' +
162
- "doesn't know what it was. This is likely due to browser " +
163
- 'flakiness. React does its best to preserve the "Pause on ' +
164
- 'exceptions" behavior of the DevTools, which requires some ' +
165
- "DEV-mode only tricks. It's possible that these don't work in " +
166
- 'your browser. Try triggering the error in production mode, ' +
167
- 'or switching to a modern browser. If you suspect that this is ' +
168
- 'actually an issue with React, please file an issue.',
169
- );
170
- } else if (isCrossOriginError) {
171
- // eslint-disable-next-line react-internal/prod-error-codes
172
- error = new Error(
173
- "A cross-origin error was thrown. React doesn't have access to " +
174
- 'the actual error object in development. ' +
175
- 'See https://react.dev/link/crossorigin-error for more information.',
176
- );
177
- }
178
- this.onError(error);
179
- }
180
-
181
- // Remove our event listeners
182
- window.removeEventListener('error', handleWindowError);
183
-
184
- if (didCall) {
185
- return;
186
- } else {
187
- // Something went really wrong, and our event was not dispatched.
188
- // https://github.com/facebook/react/issues/16734
189
- // https://github.com/facebook/react/issues/16585
190
- // Fall back to the production implementation.
191
- restoreAfterDispatch();
192
- // we fall through and call the prod version instead
193
- }
194
- }
195
- // We only get here if we are in an environment that either does not support the browser
196
- // variant or we had trouble getting the browser to emit the error.
197
- // $FlowFixMe[method-unbinding]
198
- const funcArgs = Array.prototype.slice.call(arguments, 3);
199
- try {
200
- // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
201
- func.apply(context, funcArgs);
202
- } catch (error) {
203
- this.onError(error);
204
- }
205
- } else {
206
- // $FlowFixMe[method-unbinding]
207
- const funcArgs = Array.prototype.slice.call(arguments, 3);
208
- try {
209
- // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
210
- func.apply(context, funcArgs);
211
- } catch (error) {
212
- this.onError(error);
213
- }
214
- }
215
-}
packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
+21
-2
@@ -22,6 +22,8 @@ let useEffect;
22
let useLayoutEffect;
23
let assertLog;
24
25
+let originalError;
26
+
27
// This tests shared behavior between the built-in and shim implementations of
28
// of useSyncExternalStore.
29
describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
@@ -44,6 +46,9 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
46
: 'react-dom-17/umd/react-dom.production.min.js',
47
),
48
);
49
+ // Because React 17 prints extra logs we need to ignore them.
50
+ originalError = console.error;
51
+ console.error = jest.fn();
52
}
53
54
React = require('react');
@@ -82,6 +87,12 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
87
require('use-sync-external-store/shim/with-selector').useSyncExternalStoreWithSelector;
88
});
89
90
+ afterEach(() => {
91
+ if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
92
+ console.error = originalError;
93
+ }
94
+ });
95
+
96
function Text({text}) {
97
Scheduler.log(text);
98
return text;
@@ -593,12 +604,20 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
604
'the number of nested updates to prevent infinite loops.',
605
);
606
}).toErrorDev(
596
- 'The result of getSnapshot should be cached to avoid an infinite loop',
607
+ gate(flags => flags.enableUseSyncExternalStoreShim)
608
+ ? [
609
+ 'Uncaught [',
610
+ 'The result of getSnapshot should be cached to avoid an infinite loop',
611
+ 'The above error occurred in the',
612
+ ]
613
+ : [
614
+ 'The result of getSnapshot should be cached to avoid an infinite loop',
615
+ ],
616
{
617
withoutStack: gate(flags => {
618
if (flags.enableUseSyncExternalStoreShim) {
619
// Stacks don't work when mixing the source and the npm package.
601
- return flags.source;
620
+ return flags.source ? 1 : 0;
621
}
622
return false;
623
}),
scripts/jest/shouldIgnoreConsoleError.js
+7
-19
@@ -7,14 +7,13 @@ module.exports = function shouldIgnoreConsoleError(
7
) {
8
if (__DEV__) {
9
if (typeof format === 'string') {
10
- if (format.indexOf('Error: Uncaught [') === 0) {
11
- // This looks like an uncaught error from invokeGuardedCallback() wrapper
12
- // in development that is reported by jsdom. Ignore because it's noisy.
13
- return true;
14
- }
15
- if (format.indexOf('The above error occurred') === 0) {
16
- // This looks like an error addendum from ReactFiberErrorLogger.
17
- // Ignore it too.
10
+ if (
11
+ args[0] != null &&
12
+ typeof args[0].message === 'string' &&
13
+ typeof args[0].stack === 'string'
14
+ ) {
15
+ // This looks like an error with addendum from ReactFiberErrorLogger.
16
+ // They are noisy too so we'll try to ignore them.
17
return true;
18
}
19
if (
@@ -36,17 +35,6 @@ module.exports = function shouldIgnoreConsoleError(
35
// This also gets logged by onRecoverableError, so we can ignore it.
36
return true;
37
}
39
- } else if (
40
- format != null &&
41
- typeof format.message === 'string' &&
42
- typeof format.stack === 'string' &&
43
- args.length === 0
44
- ) {
45
- if (format.stack.indexOf('Error: Uncaught [') === 0) {
46
- // This looks like an uncaught error from invokeGuardedCallback() wrapper
47
- // in development that is reported by jest-environment-jsdom. Ignore because it's noisy.
48
- return true;
49
- }
38
}
39
} else {
40
if (
scripts/rollup/forks.js
-12
@@ -232,18 +232,6 @@ const forks = Object.freeze({
232
}
233
},
234
235
- // Different wrapping/reporting for caught errors.
236
- './packages/shared/invokeGuardedCallbackImpl.js': (bundleType, entry) => {
237
- switch (bundleType) {
238
- case FB_WWW_DEV:
239
- case FB_WWW_PROD:
240
- case FB_WWW_PROFILING:
241
- return './packages/shared/forks/invokeGuardedCallbackImpl.www.js';
242
- default:
243
- return null;
244
- }
245
- },
246
-
235
// Different dialogs for caught errors.
236
'./packages/react-reconciler/src/ReactFiberErrorDialog.js': (
237
bundleType,