@samitouri / QOS-React-2 / commits / 89021fb4ec

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,