main
js 1,922 lines 53.5 KB
Raw
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 * @jest-environment node
9 */
10
11 'use strict';
12
13 let PropTypes;
14 let React;
15 let ReactNoop;
16 let Scheduler;
17 let act;
18 let assertLog;
19 let waitForAll;
20 let waitFor;
21 let waitForThrow;
22 let assertConsoleErrorDev;
23
24 describe('ReactIncrementalErrorHandling', () => {
25 beforeEach(() => {
26 jest.resetModules();
27 PropTypes = require('prop-types');
28 React = require('react');
29 ReactNoop = require('react-noop-renderer');
30 Scheduler = require('scheduler');
31 act = require('internal-test-utils').act;
32 assertConsoleErrorDev =
33 require('internal-test-utils').assertConsoleErrorDev;
34
35 const InternalTestUtils = require('internal-test-utils');
36 assertLog = InternalTestUtils.assertLog;
37 waitForAll = InternalTestUtils.waitForAll;
38 waitFor = InternalTestUtils.waitFor;
39 waitForThrow = InternalTestUtils.waitForThrow;
40 });
41
42 afterEach(() => {
43 jest.restoreAllMocks();
44 });
45
46 function normalizeCodeLocInfo(str) {
47 return (
48 str &&
49 str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
50 return '\n in ' + name + ' (at **)';
51 })
52 );
53 }
54
55 // Note: This is based on a similar component we use in www. We can delete
56 // once the extra div wrapper is no longer necessary.
57 function LegacyHiddenDiv({children, mode}) {
58 return (
59 <div hidden={mode === 'hidden'}>
60 <React.unstable_LegacyHidden
61 mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
62 {children}
63 </React.unstable_LegacyHidden>
64 </div>
65 );
66 }
67
68 it('recovers from errors asynchronously', async () => {
69 class ErrorBoundary extends React.Component {
70 state = {error: null};
71 static getDerivedStateFromError(error) {
72 Scheduler.log('getDerivedStateFromError');
73 return {error};
74 }
75 render() {
76 if (this.state.error) {
77 Scheduler.log('ErrorBoundary (catch)');
78 return <ErrorMessage error={this.state.error} />;
79 }
80 Scheduler.log('ErrorBoundary (try)');
81 return this.props.children;
82 }
83 }
84
85 function ErrorMessage({error}) {
86 Scheduler.log('ErrorMessage');
87 return <span prop={`Caught an error: ${error.message}`} />;
88 }
89
90 function Indirection({children}) {
91 Scheduler.log('Indirection');
92 return children || null;
93 }
94
95 function BadRender({unused}) {
96 Scheduler.log('throw');
97 throw new Error('oops!');
98 }
99
100 React.startTransition(() => {
101 ReactNoop.render(
102 <>
103 <ErrorBoundary>
104 <Indirection>
105 <Indirection>
106 <Indirection>
107 <BadRender />
108 </Indirection>
109 </Indirection>
110 </Indirection>
111 </ErrorBoundary>
112 <Indirection />
113 <Indirection />
114 </>,
115 );
116 });
117
118 // Start rendering asynchronously
119 await waitFor([
120 'ErrorBoundary (try)',
121 'Indirection',
122 'Indirection',
123 'Indirection',
124 // An error is thrown. React keeps rendering asynchronously.
125 'throw',
126
127 // Call getDerivedStateFromError and re-render the error boundary, this
128 // time rendering an error message.
129 'getDerivedStateFromError',
130 'ErrorBoundary (catch)',
131 'ErrorMessage',
132 ]);
133 expect(ReactNoop).toMatchRenderedOutput(null);
134
135 // The work loop unwound to the nearest error boundary. Continue rendering
136 // asynchronously.
137 await waitFor(['Indirection']);
138
139 // Since the error was thrown during an async render, React won't commit the
140 // result yet. After render we render the last child, React will attempt to
141 // render again, synchronously, just in case that happens to fix the error
142 // (i.e. as in the case of a data race). Flush just one more unit of work to
143 // demonstrate that this render is synchronous.
144 expect(ReactNoop.flushNextYield()).toEqual([
145 'Indirection',
146
147 'ErrorBoundary (try)',
148 'Indirection',
149 'Indirection',
150 'Indirection',
151
152 // The error was thrown again. This time, React will actually commit
153 // the result.
154 'throw',
155 'getDerivedStateFromError',
156 'ErrorBoundary (catch)',
157 'ErrorMessage',
158 'Indirection',
159 'Indirection',
160 ]);
161
162 expect(ReactNoop).toMatchRenderedOutput(
163 <span prop="Caught an error: oops!" />,
164 );
165 });
166
167 it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', async () => {
168 class ErrorBoundary extends React.Component {
169 state = {error: null};
170 componentDidCatch(error) {
171 Scheduler.log('componentDidCatch');
172 this.setState({error});
173 }
174 render() {
175 if (this.state.error) {
176 Scheduler.log('ErrorBoundary (catch)');
177 return <ErrorMessage error={this.state.error} />;
178 }
179 Scheduler.log('ErrorBoundary (try)');
180 return this.props.children;
181 }
182 }
183
184 function ErrorMessage({error}) {
185 Scheduler.log('ErrorMessage');
186 return <span prop={`Caught an error: ${error.message}`} />;
187 }
188
189 function Indirection({children}) {
190 Scheduler.log('Indirection');
191 return children || null;
192 }
193
194 function BadRender({unused}) {
195 Scheduler.log('throw');
196 throw new Error('oops!');
197 }
198
199 React.startTransition(() => {
200 ReactNoop.render(
201 <>
202 <ErrorBoundary>
203 <Indirection>
204 <Indirection>
205 <Indirection>
206 <BadRender />
207 </Indirection>
208 </Indirection>
209 </Indirection>
210 </ErrorBoundary>
211 <Indirection />
212 <Indirection />
213 </>,
214 );
215 });
216
217 // Start rendering asynchronously
218 await waitFor([
219 'ErrorBoundary (try)',
220 'Indirection',
221 'Indirection',
222 'Indirection',
223 // An error is thrown. React keeps rendering asynchronously.
224 'throw',
225 ]);
226
227 // Still rendering async...
228 await waitFor(['Indirection']);
229
230 await waitFor([
231 'Indirection',
232 // Now that the tree is complete, and there's no remaining work, React
233 // reverts to legacy mode to retry one more time before handling the error.
234
235 'ErrorBoundary (try)',
236 'Indirection',
237 'Indirection',
238 'Indirection',
239
240 // The error was thrown again. Now we can handle it.
241 'throw',
242 'Indirection',
243 'Indirection',
244 'componentDidCatch',
245 'ErrorBoundary (catch)',
246 'ErrorMessage',
247 ]);
248 expect(ReactNoop).toMatchRenderedOutput(
249 <span prop="Caught an error: oops!" />,
250 );
251 });
252
253 it("retries at a lower priority if there's additional pending work", async () => {
254 function App(props) {
255 if (props.isBroken) {
256 Scheduler.log('error');
257 throw new Error('Oops!');
258 }
259 Scheduler.log('success');
260 return <span prop="Everything is fine." />;
261 }
262
263 function onCommit() {
264 Scheduler.log('commit');
265 }
266
267 React.startTransition(() => {
268 ReactNoop.render(<App isBroken={true} />, onCommit);
269 });
270 await waitFor(['error']);
271
272 React.startTransition(() => {
273 // This update is in a separate batch
274 ReactNoop.render(<App isBroken={false} />, onCommit);
275 });
276
277 // React will try to recover by rendering all the pending updates in a
278 // single batch, synchronously. This time it succeeds.
279 //
280 // This tells Scheduler to render a single unit of work. Because the render
281 // to recover from the error is synchronous, this should be enough to
282 // finish the rest of the work.
283 Scheduler.unstable_flushNumberOfYields(1);
284 assertLog([
285 'success',
286 // Nothing commits until the second update completes.
287 'commit',
288 'commit',
289 ]);
290 assertConsoleErrorDev([
291 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' +
292 '\n in <stack>',
293 ]);
294 expect(ReactNoop).toMatchRenderedOutput(
295 <span prop="Everything is fine." />,
296 );
297 });
298
299 // @gate enableLegacyHidden
300 it('does not include offscreen work when retrying after an error', async () => {
301 function App(props) {
302 if (props.isBroken) {
303 Scheduler.log('error');
304 throw new Error('Oops!');
305 }
306 Scheduler.log('success');
307 return (
308 <>
309 Everything is fine
310 <LegacyHiddenDiv mode="hidden">
311 <div>Offscreen content</div>
312 </LegacyHiddenDiv>
313 </>
314 );
315 }
316
317 function onCommit() {
318 Scheduler.log('commit');
319 }
320
321 React.startTransition(() => {
322 ReactNoop.render(<App isBroken={true} />, onCommit);
323 });
324 await waitFor(['error']);
325
326 expect(ReactNoop).toMatchRenderedOutput(null);
327
328 React.startTransition(() => {
329 // This update is in a separate batch
330 ReactNoop.render(<App isBroken={false} />, onCommit);
331 });
332
333 // React will try to recover by rendering all the pending updates in a
334 // single batch, synchronously. This time it succeeds.
335 //
336 // This tells Scheduler to render a single unit of work. Because the render
337 // to recover from the error is synchronous, this should be enough to
338 // finish the rest of the work.
339 Scheduler.unstable_flushNumberOfYields(1);
340 assertLog([
341 'success',
342 // Nothing commits until the second update completes.
343 'commit',
344 'commit',
345 ]);
346 assertConsoleErrorDev([
347 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' +
348 '\n in <stack>',
349 ]);
350 // This should not include the offscreen content
351 expect(ReactNoop).toMatchRenderedOutput(
352 <>
353 Everything is fine
354 <div hidden={true} />
355 </>,
356 );
357
358 // The offscreen content finishes in a subsequent render
359 await waitForAll([]);
360 expect(ReactNoop).toMatchRenderedOutput(
361 <>
362 Everything is fine
363 <div hidden={true}>
364 <div>Offscreen content</div>
365 </div>
366 </>,
367 );
368 });
369
370 it('retries one more time before handling error', async () => {
371 function BadRender({unused}) {
372 Scheduler.log('BadRender');
373 throw new Error('oops');
374 }
375
376 function Sibling({unused}) {
377 Scheduler.log('Sibling');
378 return <span prop="Sibling" />;
379 }
380
381 function Parent({unused}) {
382 Scheduler.log('Parent');
383 return (
384 <>
385 <BadRender />
386 <Sibling />
387 </>
388 );
389 }
390
391 React.startTransition(() => {
392 ReactNoop.render(<Parent />, () => Scheduler.log('commit'));
393 });
394
395 // Render the bad component asynchronously
396 await waitFor(['Parent', 'BadRender']);
397
398 // The work loop unwound to the nearest error boundary. React will try
399 // to render one more time, synchronously. Flush just one unit of work to
400 // demonstrate that this render is synchronous.
401 Scheduler.unstable_flushNumberOfYields(1);
402 assertLog(['Parent', 'BadRender', 'commit']);
403 expect(ReactNoop).toMatchRenderedOutput(null);
404 });
405
406 it('retries one more time if an error occurs during a render that expires midway through the tree', async () => {
407 function Oops({unused}) {
408 Scheduler.log('Oops');
409 throw new Error('Oops');
410 }
411
412 function Text({text}) {
413 Scheduler.log(text);
414 return text;
415 }
416
417 function App({unused}) {
418 return (
419 <>
420 <Text text="A" />
421 <Text text="B" />
422 <Oops />
423 <Text text="C" />
424 <Text text="D" />
425 </>
426 );
427 }
428
429 React.startTransition(() => {
430 ReactNoop.render(<App />);
431 });
432
433 // Render part of the tree
434 await waitFor(['A', 'B']);
435
436 // Expire the render midway through
437 Scheduler.unstable_advanceTime(10000);
438
439 Scheduler.unstable_flushExpired();
440 ReactNoop.flushSync();
441
442 assertLog([
443 // The render expired, but we shouldn't throw out the partial work.
444 // Finish the current level.
445 'Oops',
446
447 // Since the error occurred during a partially concurrent render, we should
448 // retry one more time, synchronously.
449 'A',
450 'B',
451 'Oops',
452 ]);
453 expect(ReactNoop).toMatchRenderedOutput(null);
454 });
455
456 it('calls componentDidCatch multiple times for multiple errors', async () => {
457 let id = 0;
458 class BadMount extends React.Component {
459 componentDidMount() {
460 throw new Error(`Error ${++id}`);
461 }
462 render() {
463 Scheduler.log('BadMount');
464 return null;
465 }
466 }
467
468 class ErrorBoundary extends React.Component {
469 state = {errorCount: 0};
470 componentDidCatch(error) {
471 Scheduler.log(`componentDidCatch: ${error.message}`);
472 this.setState(state => ({errorCount: state.errorCount + 1}));
473 }
474 render() {
475 if (this.state.errorCount > 0) {
476 return <span prop={`Number of errors: ${this.state.errorCount}`} />;
477 }
478 Scheduler.log('ErrorBoundary');
479 return this.props.children;
480 }
481 }
482
483 ReactNoop.render(
484 <ErrorBoundary>
485 <BadMount />
486 <BadMount />
487 <BadMount />
488 </ErrorBoundary>,
489 );
490
491 await waitForAll([
492 'ErrorBoundary',
493 'BadMount',
494 'BadMount',
495 'BadMount',
496 'componentDidCatch: Error 1',
497 'componentDidCatch: Error 2',
498 'componentDidCatch: Error 3',
499 ]);
500 expect(ReactNoop).toMatchRenderedOutput(
501 <span prop="Number of errors: 3" />,
502 );
503 });
504
505 it('catches render error in a boundary during full deferred mounting', async () => {
506 class ErrorBoundary extends React.Component {
507 state = {error: null};
508 componentDidCatch(error) {
509 this.setState({error});
510 }
511 render() {
512 if (this.state.error) {
513 return (
514 <span prop={`Caught an error: ${this.state.error.message}.`} />
515 );
516 }
517 return this.props.children;
518 }
519 }
520
521 function BrokenRender(props) {
522 throw new Error('Hello');
523 }
524
525 ReactNoop.render(
526 <ErrorBoundary>
527 <BrokenRender />
528 </ErrorBoundary>,
529 );
530 await waitForAll([]);
531 expect(ReactNoop).toMatchRenderedOutput(
532 <span prop="Caught an error: Hello." />,
533 );
534 });
535
536 it('catches render error in a boundary during partial deferred mounting', async () => {
537 class ErrorBoundary extends React.Component {
538 state = {error: null};
539 componentDidCatch(error) {
540 Scheduler.log('ErrorBoundary componentDidCatch');
541 this.setState({error});
542 }
543 render() {
544 if (this.state.error) {
545 Scheduler.log('ErrorBoundary render error');
546 return (
547 <span prop={`Caught an error: ${this.state.error.message}.`} />
548 );
549 }
550 Scheduler.log('ErrorBoundary render success');
551 return this.props.children;
552 }
553 }
554
555 function BrokenRender({unused}) {
556 Scheduler.log('BrokenRender');
557 throw new Error('Hello');
558 }
559
560 React.startTransition(() => {
561 ReactNoop.render(
562 <ErrorBoundary>
563 <BrokenRender />
564 </ErrorBoundary>,
565 );
566 });
567
568 await waitFor(['ErrorBoundary render success']);
569 expect(ReactNoop).toMatchRenderedOutput(null);
570
571 await waitForAll([
572 'BrokenRender',
573 // React retries one more time
574 'ErrorBoundary render success',
575
576 // Errored again on retry. Now handle it.
577 'BrokenRender',
578 'ErrorBoundary componentDidCatch',
579 'ErrorBoundary render error',
580 ]);
581 expect(ReactNoop).toMatchRenderedOutput(
582 <span prop="Caught an error: Hello." />,
583 );
584 });
585
586 it('catches render error in a boundary during synchronous mounting', () => {
587 class ErrorBoundary extends React.Component {
588 state = {error: null};
589 componentDidCatch(error) {
590 Scheduler.log('ErrorBoundary componentDidCatch');
591 this.setState({error});
592 }
593 render() {
594 if (this.state.error) {
595 Scheduler.log('ErrorBoundary render error');
596 return (
597 <span prop={`Caught an error: ${this.state.error.message}.`} />
598 );
599 }
600 Scheduler.log('ErrorBoundary render success');
601 return this.props.children;
602 }
603 }
604
605 function BrokenRender({unused}) {
606 Scheduler.log('BrokenRender');
607 throw new Error('Hello');
608 }
609
610 ReactNoop.flushSync(() => {
611 ReactNoop.render(
612 <ErrorBoundary>
613 <BrokenRender />
614 </ErrorBoundary>,
615 );
616 });
617
618 assertLog([
619 'ErrorBoundary render success',
620 'BrokenRender',
621
622 // React retries one more time
623 'ErrorBoundary render success',
624 'BrokenRender',
625
626 // Errored again on retry. Now handle it.
627 'ErrorBoundary componentDidCatch',
628 'ErrorBoundary render error',
629 ]);
630 expect(ReactNoop).toMatchRenderedOutput(
631 <span prop="Caught an error: Hello." />,
632 );
633 });
634
635 it('catches render error in a boundary during batched mounting', () => {
636 class ErrorBoundary extends React.Component {
637 state = {error: null};
638 componentDidCatch(error) {
639 Scheduler.log('ErrorBoundary componentDidCatch');
640 this.setState({error});
641 }
642 render() {
643 if (this.state.error) {
644 Scheduler.log('ErrorBoundary render error');
645 return (
646 <span prop={`Caught an error: ${this.state.error.message}.`} />
647 );
648 }
649 Scheduler.log('ErrorBoundary render success');
650 return this.props.children;
651 }
652 }
653
654 function BrokenRender({unused}) {
655 Scheduler.log('BrokenRender');
656 throw new Error('Hello');
657 }
658
659 ReactNoop.flushSync(() => {
660 ReactNoop.render(<ErrorBoundary>Before the storm.</ErrorBoundary>);
661 ReactNoop.render(
662 <ErrorBoundary>
663 <BrokenRender />
664 </ErrorBoundary>,
665 );
666 });
667
668 assertLog([
669 'ErrorBoundary render success',
670 'BrokenRender',
671
672 // React retries one more time
673 'ErrorBoundary render success',
674 'BrokenRender',
675
676 // Errored again on retry. Now handle it.
677 'ErrorBoundary componentDidCatch',
678 'ErrorBoundary render error',
679 ]);
680 expect(ReactNoop).toMatchRenderedOutput(
681 <span prop="Caught an error: Hello." />,
682 );
683 });
684
685 it('propagates an error from a noop error boundary during full deferred mounting', async () => {
686 class RethrowErrorBoundary extends React.Component {
687 componentDidCatch(error) {
688 Scheduler.log('RethrowErrorBoundary componentDidCatch');
689 throw error;
690 }
691 render() {
692 Scheduler.log('RethrowErrorBoundary render');
693 return this.props.children;
694 }
695 }
696
697 function BrokenRender({unused}) {
698 Scheduler.log('BrokenRender');
699 throw new Error('Hello');
700 }
701
702 ReactNoop.render(
703 <RethrowErrorBoundary>
704 <BrokenRender />
705 </RethrowErrorBoundary>,
706 );
707
708 await waitForThrow('Hello');
709 assertLog([
710 'RethrowErrorBoundary render',
711 'BrokenRender',
712
713 // React retries one more time
714 'RethrowErrorBoundary render',
715 'BrokenRender',
716
717 // Errored again on retry. Now handle it.
718 'RethrowErrorBoundary componentDidCatch',
719 ]);
720 expect(ReactNoop.getChildrenAsJSX()).toEqual(null);
721 });
722
723 it('propagates an error from a noop error boundary during partial deferred mounting', async () => {
724 class RethrowErrorBoundary extends React.Component {
725 componentDidCatch(error) {
726 Scheduler.log('RethrowErrorBoundary componentDidCatch');
727 throw error;
728 }
729 render() {
730 Scheduler.log('RethrowErrorBoundary render');
731 return this.props.children;
732 }
733 }
734
735 function BrokenRender({unused}) {
736 Scheduler.log('BrokenRender');
737 throw new Error('Hello');
738 }
739
740 React.startTransition(() => {
741 ReactNoop.render(
742 <RethrowErrorBoundary>
743 <BrokenRender />
744 </RethrowErrorBoundary>,
745 );
746 });
747
748 await waitFor(['RethrowErrorBoundary render']);
749
750 await waitForThrow('Hello');
751 assertLog([
752 'BrokenRender',
753
754 // React retries one more time
755 'RethrowErrorBoundary render',
756 'BrokenRender',
757
758 // Errored again on retry. Now handle it.
759 'RethrowErrorBoundary componentDidCatch',
760 ]);
761 expect(ReactNoop).toMatchRenderedOutput(null);
762 });
763
764 it('propagates an error from a noop error boundary during synchronous mounting', () => {
765 class RethrowErrorBoundary extends React.Component {
766 componentDidCatch(error) {
767 Scheduler.log('RethrowErrorBoundary componentDidCatch');
768 throw error;
769 }
770 render() {
771 Scheduler.log('RethrowErrorBoundary render');
772 return this.props.children;
773 }
774 }
775
776 function BrokenRender({unused}) {
777 Scheduler.log('BrokenRender');
778 throw new Error('Hello');
779 }
780
781 ReactNoop.flushSync(() => {
782 ReactNoop.render(
783 <RethrowErrorBoundary>
784 <BrokenRender />
785 </RethrowErrorBoundary>,
786 );
787 });
788
789 assertLog([
790 'RethrowErrorBoundary render',
791 'BrokenRender',
792
793 // React retries one more time
794 'RethrowErrorBoundary render',
795 'BrokenRender',
796
797 // Errored again on retry. Now handle it.
798 'RethrowErrorBoundary componentDidCatch',
799 ]);
800 expect(ReactNoop).toMatchRenderedOutput(null);
801 });
802
803 it('propagates an error from a noop error boundary during batched mounting', () => {
804 class RethrowErrorBoundary extends React.Component {
805 componentDidCatch(error) {
806 Scheduler.log('RethrowErrorBoundary componentDidCatch');
807 throw error;
808 }
809 render() {
810 Scheduler.log('RethrowErrorBoundary render');
811 return this.props.children;
812 }
813 }
814
815 function BrokenRender({unused}) {
816 Scheduler.log('BrokenRender');
817 throw new Error('Hello');
818 }
819
820 ReactNoop.flushSync(() => {
821 ReactNoop.render(
822 <RethrowErrorBoundary>Before the storm.</RethrowErrorBoundary>,
823 );
824 ReactNoop.render(
825 <RethrowErrorBoundary>
826 <BrokenRender />
827 </RethrowErrorBoundary>,
828 );
829 });
830
831 assertLog([
832 'RethrowErrorBoundary render',
833 'BrokenRender',
834
835 // React retries one more time
836 'RethrowErrorBoundary render',
837 'BrokenRender',
838
839 // Errored again on retry. Now handle it.
840 'RethrowErrorBoundary componentDidCatch',
841 ]);
842 expect(ReactNoop).toMatchRenderedOutput(null);
843 });
844
845 it('applies batched updates regardless despite errors in scheduling', async () => {
846 ReactNoop.render(<span prop="a:1" />);
847 expect(() => {
848 ReactNoop.batchedUpdates(() => {
849 ReactNoop.render(<span prop="a:2" />);
850 ReactNoop.render(<span prop="a:3" />);
851 throw new Error('Hello');
852 });
853 }).toThrow('Hello');
854 await waitForAll([]);
855 expect(ReactNoop).toMatchRenderedOutput(<span prop="a:3" />);
856 });
857
858 it('applies nested batched updates despite errors in scheduling', async () => {
859 ReactNoop.render(<span prop="a:1" />);
860 expect(() => {
861 ReactNoop.batchedUpdates(() => {
862 ReactNoop.render(<span prop="a:2" />);
863 ReactNoop.render(<span prop="a:3" />);
864 ReactNoop.batchedUpdates(() => {
865 ReactNoop.render(<span prop="a:4" />);
866 ReactNoop.render(<span prop="a:5" />);
867 throw new Error('Hello');
868 });
869 });
870 }).toThrow('Hello');
871 await waitForAll([]);
872 expect(ReactNoop).toMatchRenderedOutput(<span prop="a:5" />);
873 });
874
875 // TODO: Is this a breaking change?
876 it('defers additional sync work to a separate event after an error', async () => {
877 ReactNoop.render(<span prop="a:1" />);
878 expect(() => {
879 ReactNoop.flushSync(() => {
880 ReactNoop.batchedUpdates(() => {
881 ReactNoop.render(<span prop="a:2" />);
882 ReactNoop.render(<span prop="a:3" />);
883 throw new Error('Hello');
884 });
885 });
886 }).toThrow('Hello');
887 await waitForAll([]);
888 expect(ReactNoop).toMatchRenderedOutput(<span prop="a:3" />);
889 });
890
891 it('can schedule updates after uncaught error in render on mount', async () => {
892 function BrokenRender({unused}) {
893 Scheduler.log('BrokenRender');
894 throw new Error('Hello');
895 }
896
897 function Foo({unused}) {
898 Scheduler.log('Foo');
899 return null;
900 }
901
902 ReactNoop.render(<BrokenRender />);
903 await waitForThrow('Hello');
904 ReactNoop.render(<Foo />);
905 assertLog([
906 'BrokenRender',
907 // React retries one more time
908 'BrokenRender',
909 // Errored again on retry
910 ]);
911 await waitForAll(['Foo']);
912 });
913
914 it('can schedule updates after uncaught error in render on update', async () => {
915 function BrokenRender({shouldThrow}) {
916 Scheduler.log('BrokenRender');
917 if (shouldThrow) {
918 throw new Error('Hello');
919 }
920 return null;
921 }
922
923 function Foo({unused}) {
924 Scheduler.log('Foo');
925 return null;
926 }
927
928 ReactNoop.render(<BrokenRender shouldThrow={false} />);
929 await waitForAll(['BrokenRender']);
930
931 ReactNoop.render(<BrokenRender shouldThrow={true} />);
932 await waitForThrow('Hello');
933 assertLog([
934 'BrokenRender',
935 // React retries one more time
936 'BrokenRender',
937 // Errored again on retry
938 ]);
939
940 ReactNoop.render(<Foo />);
941 await waitForAll(['Foo']);
942 });
943
944 it('can schedule updates after uncaught error during unmounting', async () => {
945 class BrokenComponentWillUnmount extends React.Component {
946 render() {
947 return <div />;
948 }
949 componentWillUnmount() {
950 throw new Error('Hello');
951 }
952 }
953
954 function Foo() {
955 Scheduler.log('Foo');
956 return null;
957 }
958
959 ReactNoop.render(<BrokenComponentWillUnmount />);
960 await waitForAll([]);
961
962 ReactNoop.render(<div />);
963 await waitForThrow('Hello');
964
965 ReactNoop.render(<Foo />);
966 await waitForAll(['Foo']);
967 });
968
969 it('should not attempt to recover an unmounting error boundary', async () => {
970 class Parent extends React.Component {
971 componentWillUnmount() {
972 Scheduler.log('Parent componentWillUnmount');
973 }
974 render() {
975 return <Boundary />;
976 }
977 }
978
979 class Boundary extends React.Component {
980 componentDidCatch(e) {
981 Scheduler.log(`Caught error: ${e.message}`);
982 }
983 render() {
984 return <ThrowsOnUnmount />;
985 }
986 }
987
988 class ThrowsOnUnmount extends React.Component {
989 componentWillUnmount() {
990 Scheduler.log('ThrowsOnUnmount componentWillUnmount');
991 throw new Error('unmount error');
992 }
993 render() {
994 return null;
995 }
996 }
997
998 ReactNoop.render(<Parent />);
999 await waitForAll([]);
1000
1001 // Because the error boundary is also unmounting,
1002 // an error in ThrowsOnUnmount should be rethrown.
1003 ReactNoop.render(null);
1004 await waitForThrow('unmount error');
1005 await assertLog([
1006 'Parent componentWillUnmount',
1007 'ThrowsOnUnmount componentWillUnmount',
1008 ]);
1009
1010 ReactNoop.render(<Parent />);
1011 });
1012
1013 it('can unmount an error boundary before it is handled', async () => {
1014 let parent;
1015
1016 class Parent extends React.Component {
1017 state = {step: 0};
1018 render() {
1019 parent = this;
1020 return this.state.step === 0 ? <Boundary /> : null;
1021 }
1022 }
1023
1024 class Boundary extends React.Component {
1025 componentDidCatch() {}
1026 render() {
1027 return <Child />;
1028 }
1029 }
1030
1031 class Child extends React.Component {
1032 componentDidUpdate() {
1033 parent.setState({step: 1});
1034 throw new Error('update error');
1035 }
1036 render() {
1037 return null;
1038 }
1039 }
1040
1041 ReactNoop.render(<Parent />);
1042 await waitForAll([]);
1043
1044 ReactNoop.flushSync(() => {
1045 ReactNoop.render(<Parent />);
1046 });
1047 });
1048
1049 it('continues work on other roots despite caught errors', async () => {
1050 class ErrorBoundary extends React.Component {
1051 state = {error: null};
1052 componentDidCatch(error) {
1053 this.setState({error});
1054 }
1055 render() {
1056 if (this.state.error) {
1057 return (
1058 <span prop={`Caught an error: ${this.state.error.message}.`} />
1059 );
1060 }
1061 return this.props.children;
1062 }
1063 }
1064
1065 function BrokenRender(props) {
1066 throw new Error('Hello');
1067 }
1068
1069 ReactNoop.renderToRootWithID(
1070 <ErrorBoundary>
1071 <BrokenRender />
1072 </ErrorBoundary>,
1073 'a',
1074 );
1075 ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
1076 await waitForAll([]);
1077 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(
1078 <span prop="Caught an error: Hello." />,
1079 );
1080 await waitForAll([]);
1081 expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:1" />);
1082 });
1083
1084 it('continues work on other roots despite uncaught errors', async () => {
1085 function BrokenRender(props) {
1086 throw new Error(props.label);
1087 }
1088
1089 ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
1090 await waitForThrow('a');
1091 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
1092
1093 ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
1094 ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
1095 await waitForThrow('a');
1096
1097 await waitForAll([]);
1098 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
1099 expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:2" />);
1100
1101 ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a');
1102 ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
1103 await waitForThrow('b');
1104 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:3" />);
1105 expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
1106
1107 ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
1108 ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
1109 ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c');
1110 await waitForThrow('b');
1111 await waitForAll([]);
1112 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:4" />);
1113 expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
1114 expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:4" />);
1115
1116 ReactNoop.renderToRootWithID(<span prop="a:5" />, 'a');
1117 ReactNoop.renderToRootWithID(<span prop="b:5" />, 'b');
1118 ReactNoop.renderToRootWithID(<span prop="c:5" />, 'c');
1119 ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd');
1120 ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
1121 await waitForThrow('e');
1122 await waitForAll([]);
1123 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:5" />);
1124 expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:5" />);
1125 expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:5" />);
1126 expect(ReactNoop.getChildrenAsJSX('d')).toEqual(<span prop="d:5" />);
1127 expect(ReactNoop.getChildrenAsJSX('e')).toEqual(null);
1128
1129 ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
1130 await waitForThrow('a');
1131
1132 ReactNoop.renderToRootWithID(<span prop="b:6" />, 'b');
1133 ReactNoop.renderToRootWithID(<BrokenRender label="c" />, 'c');
1134 await waitForThrow('c');
1135
1136 ReactNoop.renderToRootWithID(<span prop="d:6" />, 'd');
1137 ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
1138 ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f');
1139 await waitForThrow('e');
1140
1141 await waitForAll([]);
1142 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
1143 expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:6" />);
1144 expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null);
1145 expect(ReactNoop.getChildrenAsJSX('d')).toEqual(<span prop="d:6" />);
1146 expect(ReactNoop.getChildrenAsJSX('e')).toEqual(null);
1147 expect(ReactNoop.getChildrenAsJSX('f')).toEqual(<span prop="f:6" />);
1148
1149 ReactNoop.unmountRootWithID('a');
1150 ReactNoop.unmountRootWithID('b');
1151 ReactNoop.unmountRootWithID('c');
1152 ReactNoop.unmountRootWithID('d');
1153 ReactNoop.unmountRootWithID('e');
1154 ReactNoop.unmountRootWithID('f');
1155 await waitForAll([]);
1156 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
1157 expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
1158 expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null);
1159 expect(ReactNoop.getChildrenAsJSX('d')).toEqual(null);
1160 expect(ReactNoop.getChildrenAsJSX('e')).toEqual(null);
1161 expect(ReactNoop.getChildrenAsJSX('f')).toEqual(null);
1162 });
1163
1164 // NOTE: When legacy context is removed, it's probably fine to just delete
1165 // this test. There's plenty of test coverage of stack unwinding in general
1166 // because it's used for new context, suspense, and many other features.
1167 // It has to be tested independently for each feature anyway. So although it
1168 // doesn't look like it, this test is specific to legacy context.
1169 // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
1170 it('unwinds the context stack correctly on error', async () => {
1171 class Provider extends React.Component {
1172 static childContextTypes = {message: PropTypes.string};
1173 static contextTypes = {message: PropTypes.string};
1174 getChildContext() {
1175 return {
1176 message: (this.context.message || '') + this.props.message,
1177 };
1178 }
1179 render() {
1180 return this.props.children;
1181 }
1182 }
1183
1184 function Connector(props, context) {
1185 return <span prop={context.message} />;
1186 }
1187
1188 Connector.contextTypes = {
1189 message: PropTypes.string,
1190 };
1191
1192 function BadRender() {
1193 throw new Error('render error');
1194 }
1195
1196 class Boundary extends React.Component {
1197 state = {error: null};
1198 componentDidCatch(error) {
1199 this.setState({error});
1200 }
1201 render() {
1202 return (
1203 <Provider message="b">
1204 <Provider message="c">
1205 <Provider message="d">
1206 <Provider message="e">
1207 {!this.state.error && <BadRender />}
1208 </Provider>
1209 </Provider>
1210 </Provider>
1211 </Provider>
1212 );
1213 }
1214 }
1215
1216 ReactNoop.render(
1217 <Provider message="a">
1218 <Boundary />
1219 <Connector />
1220 </Provider>,
1221 );
1222
1223 await waitForAll([]);
1224 assertConsoleErrorDev([
1225 'Provider uses the legacy childContextTypes API which will soon be removed. ' +
1226 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1227 ' in Provider (at **)',
1228 'Provider uses the legacy contextTypes API which will soon be removed. ' +
1229 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1230 ' in Provider (at **)',
1231 'Connector uses the legacy contextTypes API which will be removed soon. ' +
1232 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
1233 ' in Connector (at **)',
1234 ]);
1235
1236 // If the context stack does not unwind, span will get 'abcde'
1237 expect(ReactNoop).toMatchRenderedOutput(<span prop="a" />);
1238 });
1239
1240 it('catches reconciler errors in a boundary during mounting', async () => {
1241 class ErrorBoundary extends React.Component {
1242 state = {error: null};
1243 componentDidCatch(error) {
1244 this.setState({error});
1245 }
1246 render() {
1247 if (this.state.error) {
1248 return <span prop={this.state.error.message} />;
1249 }
1250 return this.props.children;
1251 }
1252 }
1253 const InvalidType = undefined;
1254 function BrokenRender(props) {
1255 return <InvalidType />;
1256 }
1257
1258 ReactNoop.render(
1259 <ErrorBoundary>
1260 <BrokenRender />
1261 </ErrorBoundary>,
1262 );
1263 await waitForAll([]);
1264
1265 expect(ReactNoop).toMatchRenderedOutput(
1266 <span
1267 prop={
1268 'Element type is invalid: expected a string (for built-in components) or ' +
1269 'a class/function (for composite components) but got: undefined.' +
1270 (__DEV__
1271 ? " You likely forgot to export your component from the file it's " +
1272 'defined in, or you might have mixed up default and named imports.' +
1273 '\n\nCheck the render method of `BrokenRender`.'
1274 : '')
1275 }
1276 />,
1277 );
1278 });
1279
1280 it('catches reconciler errors in a boundary during update', async () => {
1281 class ErrorBoundary extends React.Component {
1282 state = {error: null};
1283 componentDidCatch(error) {
1284 this.setState({error});
1285 }
1286 render() {
1287 if (this.state.error) {
1288 return <span prop={this.state.error.message} />;
1289 }
1290 return this.props.children;
1291 }
1292 }
1293
1294 const InvalidType = undefined;
1295 function BrokenRender(props) {
1296 return props.fail ? <InvalidType /> : <span />;
1297 }
1298
1299 ReactNoop.render(
1300 <ErrorBoundary>
1301 <BrokenRender fail={false} />
1302 </ErrorBoundary>,
1303 );
1304 await waitForAll([]);
1305
1306 ReactNoop.render(
1307 <ErrorBoundary>
1308 <BrokenRender fail={true} />
1309 </ErrorBoundary>,
1310 );
1311 await waitForAll([]);
1312
1313 expect(ReactNoop).toMatchRenderedOutput(
1314 <span
1315 prop={
1316 'Element type is invalid: expected a string (for built-in components) or ' +
1317 'a class/function (for composite components) but got: undefined.' +
1318 (__DEV__
1319 ? " You likely forgot to export your component from the file it's " +
1320 'defined in, or you might have mixed up default and named imports.' +
1321 '\n\nCheck the render method of `BrokenRender`.'
1322 : '')
1323 }
1324 />,
1325 );
1326 });
1327
1328 it('recovers from uncaught reconciler errors', async () => {
1329 const InvalidType = undefined;
1330 ReactNoop.render(<InvalidType />);
1331
1332 await waitForThrow(
1333 'Element type is invalid: expected a string (for built-in components) or ' +
1334 'a class/function (for composite components) but got: undefined.' +
1335 (__DEV__
1336 ? " You likely forgot to export your component from the file it's " +
1337 'defined in, or you might have mixed up default and named imports.'
1338 : ''),
1339 );
1340
1341 ReactNoop.render(<span prop="hi" />);
1342 await waitForAll([]);
1343 expect(ReactNoop).toMatchRenderedOutput(<span prop="hi" />);
1344 });
1345
1346 it('unmounts components with uncaught errors', async () => {
1347 let inst;
1348
1349 class BrokenRenderAndUnmount extends React.Component {
1350 state = {fail: false};
1351 componentWillUnmount() {
1352 Scheduler.log('BrokenRenderAndUnmount componentWillUnmount');
1353 }
1354 render() {
1355 inst = this;
1356 if (this.state.fail) {
1357 throw new Error('Hello.');
1358 }
1359 return null;
1360 }
1361 }
1362
1363 class Parent extends React.Component {
1364 componentWillUnmount() {
1365 Scheduler.log('Parent componentWillUnmount [!]');
1366 throw new Error('One does not simply unmount me.');
1367 }
1368 render() {
1369 return this.props.children;
1370 }
1371 }
1372
1373 ReactNoop.render(
1374 <Parent>
1375 <Parent>
1376 <BrokenRenderAndUnmount />
1377 </Parent>
1378 </Parent>,
1379 );
1380 await waitForAll([]);
1381
1382 let aggregateError;
1383 try {
1384 await act(() => {
1385 ReactNoop.flushSync(() => {
1386 inst.setState({fail: true});
1387 });
1388 });
1389 } catch (e) {
1390 aggregateError = e;
1391 }
1392
1393 assertLog([
1394 // Attempt to clean up.
1395 // Errors in parents shouldn't stop children from unmounting.
1396 'Parent componentWillUnmount [!]',
1397 'Parent componentWillUnmount [!]',
1398 'BrokenRenderAndUnmount componentWillUnmount',
1399 ]);
1400 expect(ReactNoop).toMatchRenderedOutput(null);
1401
1402 // React threw both errors as a single AggregateError
1403 const errors = aggregateError.errors;
1404 expect(errors.length).toBe(3);
1405 expect(errors[0].message).toBe('Hello.');
1406 expect(errors[1].message).toBe('One does not simply unmount me.');
1407 expect(errors[2].message).toBe('One does not simply unmount me.');
1408 });
1409
1410 it('does not interrupt unmounting if detaching a ref throws', async () => {
1411 class Bar extends React.Component {
1412 componentWillUnmount() {
1413 Scheduler.log('Bar unmount');
1414 }
1415 render() {
1416 return <span prop="Bar" />;
1417 }
1418 }
1419
1420 function barRef(inst) {
1421 if (inst === null) {
1422 Scheduler.log('barRef detach');
1423 throw new Error('Detach error');
1424 }
1425 Scheduler.log('barRef attach');
1426 }
1427
1428 function Foo(props) {
1429 return <div>{props.hide ? null : <Bar ref={barRef} />}</div>;
1430 }
1431
1432 ReactNoop.render(<Foo />);
1433 await waitForAll(['barRef attach']);
1434 expect(ReactNoop).toMatchRenderedOutput(
1435 <div>
1436 <span prop="Bar" />
1437 </div>,
1438 );
1439
1440 // Unmount
1441 ReactNoop.render(<Foo hide={true} />);
1442 await waitForThrow('Detach error');
1443 assertLog([
1444 'barRef detach',
1445 // Bar should unmount even though its ref threw an error while detaching
1446 'Bar unmount',
1447 ]);
1448 // Because there was an error, entire tree should unmount
1449 expect(ReactNoop).toMatchRenderedOutput(null);
1450 });
1451
1452 it('handles error thrown by host config while working on failed root', async () => {
1453 ReactNoop.render(<errorInBeginPhase />);
1454 await waitForThrow('Error in host config.');
1455 });
1456
1457 it('handles error thrown by top-level callback', async () => {
1458 ReactNoop.render(<div />, () => {
1459 throw new Error('Error!');
1460 });
1461 await waitForThrow('Error!');
1462 });
1463
1464 it('error boundaries capture non-errors', async () => {
1465 spyOnProd(console, 'error').mockImplementation(() => {});
1466 spyOnDev(console, 'error').mockImplementation(() => {});
1467
1468 class ErrorBoundary extends React.Component {
1469 state = {error: null};
1470 componentDidCatch(error) {
1471 // Should not be called
1472 Scheduler.log('componentDidCatch');
1473 this.setState({error});
1474 }
1475 render() {
1476 if (this.state.error) {
1477 Scheduler.log('ErrorBoundary (catch)');
1478 return (
1479 <span
1480 prop={`Caught an error: ${this.state.error.nonStandardMessage}`}
1481 />
1482 );
1483 }
1484 Scheduler.log('ErrorBoundary (try)');
1485 return this.props.children;
1486 }
1487 }
1488
1489 function Indirection({children}) {
1490 Scheduler.log('Indirection');
1491 return children;
1492 }
1493
1494 const notAnError = {nonStandardMessage: 'oops'};
1495 function BadRender({unused}) {
1496 Scheduler.log('BadRender');
1497 throw notAnError;
1498 }
1499
1500 ReactNoop.render(
1501 <ErrorBoundary>
1502 <Indirection>
1503 <BadRender />
1504 </Indirection>
1505 </ErrorBoundary>,
1506 );
1507
1508 await waitForAll([
1509 'ErrorBoundary (try)',
1510 'Indirection',
1511 'BadRender',
1512
1513 // React retries one more time
1514 'ErrorBoundary (try)',
1515 'Indirection',
1516 'BadRender',
1517
1518 // Errored again on retry. Now handle it.
1519 'componentDidCatch',
1520 'ErrorBoundary (catch)',
1521 ]);
1522 expect(ReactNoop).toMatchRenderedOutput(
1523 <span prop="Caught an error: oops" />,
1524 );
1525
1526 if (__DEV__) {
1527 expect(console.error).toHaveBeenCalledTimes(1);
1528 expect(console.error.mock.calls[0][1]).toBe(notAnError);
1529 expect(console.error.mock.calls[0][2]).toContain(
1530 'The above error occurred in the <BadRender> component',
1531 );
1532 } else {
1533 expect(console.error).toHaveBeenCalledTimes(1);
1534 expect(console.error.mock.calls[0][0]).toBe(notAnError);
1535 }
1536 });
1537
1538 // TODO: Error boundary does not catch promises
1539
1540 it('continues working on siblings of a component that throws', async () => {
1541 class ErrorBoundary extends React.Component {
1542 state = {error: null};
1543 componentDidCatch(error) {
1544 Scheduler.log('componentDidCatch');
1545 this.setState({error});
1546 }
1547 render() {
1548 if (this.state.error) {
1549 Scheduler.log('ErrorBoundary (catch)');
1550 return <ErrorMessage error={this.state.error} />;
1551 }
1552 Scheduler.log('ErrorBoundary (try)');
1553 return this.props.children;
1554 }
1555 }
1556
1557 function ErrorMessage({error}) {
1558 Scheduler.log('ErrorMessage');
1559 return <span prop={`Caught an error: ${error.message}`} />;
1560 }
1561
1562 function BadRenderSibling({unused}) {
1563 Scheduler.log('BadRenderSibling');
1564 return null;
1565 }
1566
1567 function BadRender({unused}) {
1568 Scheduler.log('throw');
1569 throw new Error('oops!');
1570 }
1571
1572 ReactNoop.render(
1573 <ErrorBoundary>
1574 <BadRender />
1575 <BadRenderSibling />
1576 <BadRenderSibling />
1577 </ErrorBoundary>,
1578 );
1579
1580 await waitForAll([
1581 'ErrorBoundary (try)',
1582 'throw',
1583 // Continue rendering siblings after BadRender throws
1584
1585 // React retries one more time
1586 'ErrorBoundary (try)',
1587 'throw',
1588
1589 // Errored again on retry. Now handle it.
1590 'componentDidCatch',
1591 'ErrorBoundary (catch)',
1592 'ErrorMessage',
1593 ]);
1594 expect(ReactNoop).toMatchRenderedOutput(
1595 <span prop="Caught an error: oops!" />,
1596 );
1597 });
1598
1599 it('calls the correct lifecycles on the error boundary after catching an error (mixed)', async () => {
1600 // This test seems a bit contrived, but it's based on an actual regression
1601 // where we checked for the existence of didUpdate instead of didMount, and
1602 // didMount was not defined.
1603 function BadRender({unused}) {
1604 Scheduler.log('throw');
1605 throw new Error('oops!');
1606 }
1607
1608 class Parent extends React.Component {
1609 state = {error: null, other: false};
1610 componentDidCatch(error) {
1611 Scheduler.log('did catch');
1612 this.setState({error});
1613 }
1614 componentDidUpdate() {
1615 Scheduler.log('did update');
1616 }
1617 render() {
1618 if (this.state.error) {
1619 Scheduler.log('render error message');
1620 return <span prop={`Caught an error: ${this.state.error.message}`} />;
1621 }
1622 Scheduler.log('render');
1623 return <BadRender />;
1624 }
1625 }
1626
1627 ReactNoop.render(<Parent step={1} />);
1628 await waitFor([
1629 'render',
1630 'throw',
1631 'render',
1632 'throw',
1633 'did catch',
1634 'render error message',
1635 'did update',
1636 ]);
1637 expect(ReactNoop).toMatchRenderedOutput(
1638 <span prop="Caught an error: oops!" />,
1639 );
1640 });
1641
1642 it('provides component stack to the error boundary with componentDidCatch', async () => {
1643 class ErrorBoundary extends React.Component {
1644 state = {error: null, errorInfo: null};
1645 componentDidCatch(error, errorInfo) {
1646 this.setState({error, errorInfo});
1647 }
1648 render() {
1649 if (this.state.errorInfo) {
1650 Scheduler.log('render error message');
1651 return (
1652 <span
1653 prop={`Caught an error:${normalizeCodeLocInfo(
1654 this.state.errorInfo.componentStack,
1655 )}.`}
1656 />
1657 );
1658 }
1659 return this.props.children;
1660 }
1661 }
1662
1663 function BrokenRender(props) {
1664 throw new Error('Hello');
1665 }
1666
1667 ReactNoop.render(
1668 <ErrorBoundary>
1669 <BrokenRender />
1670 </ErrorBoundary>,
1671 );
1672 await waitForAll(['render error message']);
1673 expect(ReactNoop).toMatchRenderedOutput(
1674 <span
1675 prop={
1676 'Caught an error:\n' +
1677 ' in BrokenRender (at **)\n' +
1678 ' in ErrorBoundary (at **).'
1679 }
1680 />,
1681 );
1682 });
1683
1684 it('does not provide component stack to the error boundary with getDerivedStateFromError', async () => {
1685 class ErrorBoundary extends React.Component {
1686 state = {error: null};
1687 static getDerivedStateFromError(error, errorInfo) {
1688 expect(errorInfo).toBeUndefined();
1689 return {error};
1690 }
1691 render() {
1692 if (this.state.error) {
1693 return <span prop={`Caught an error: ${this.state.error.message}`} />;
1694 }
1695 return this.props.children;
1696 }
1697 }
1698
1699 function BrokenRender(props) {
1700 throw new Error('Hello');
1701 }
1702
1703 ReactNoop.render(
1704 <ErrorBoundary>
1705 <BrokenRender />
1706 </ErrorBoundary>,
1707 );
1708 await waitForAll([]);
1709 expect(ReactNoop).toMatchRenderedOutput(
1710 <span prop="Caught an error: Hello" />,
1711 );
1712 });
1713
1714 it('provides component stack even if overriding prepareStackTrace', async () => {
1715 Error.prepareStackTrace = function (error, callsites) {
1716 const stack = ['An error occurred:', error.message];
1717 for (let i = 0; i < callsites.length; i++) {
1718 const callsite = callsites[i];
1719 stack.push(
1720 '\t' + callsite.getFunctionName(),
1721 '\t\tat ' + callsite.getFileName(),
1722 '\t\ton line ' + callsite.getLineNumber(),
1723 );
1724 }
1725
1726 return stack.join('\n');
1727 };
1728
1729 class ErrorBoundary extends React.Component {
1730 state = {error: null, errorInfo: null};
1731 componentDidCatch(error, errorInfo) {
1732 this.setState({error, errorInfo});
1733 }
1734 render() {
1735 if (this.state.errorInfo) {
1736 Scheduler.log('render error message');
1737 return (
1738 <span
1739 prop={`Caught an error:${normalizeCodeLocInfo(
1740 this.state.errorInfo.componentStack,
1741 )}.`}
1742 />
1743 );
1744 }
1745 return this.props.children;
1746 }
1747 }
1748
1749 function BrokenRender(props) {
1750 throw new Error('Hello');
1751 }
1752
1753 ReactNoop.render(
1754 <ErrorBoundary>
1755 <BrokenRender />
1756 </ErrorBoundary>,
1757 );
1758 await waitForAll(['render error message']);
1759 Error.prepareStackTrace = undefined;
1760
1761 expect(ReactNoop).toMatchRenderedOutput(
1762 <span
1763 prop={
1764 'Caught an error:\n' +
1765 ' in BrokenRender (at **)\n' +
1766 ' in ErrorBoundary (at **).'
1767 }
1768 />,
1769 );
1770 });
1771
1772 it('uncaught errors should be discarded if the render is aborted', async () => {
1773 const root = ReactNoop.createRoot();
1774
1775 function Oops({unused}) {
1776 Scheduler.log('Oops');
1777 throw Error('Oops');
1778 }
1779
1780 await act(async () => {
1781 React.startTransition(() => {
1782 root.render(<Oops />);
1783 });
1784
1785 // Render past the component that throws, then yield.
1786 await waitFor(['Oops']);
1787 expect(root).toMatchRenderedOutput(null);
1788 // Interleaved update. When the root completes, instead of throwing the
1789 // error, it should try rendering again. This update will cause it to
1790 // recover gracefully.
1791 React.startTransition(() => {
1792 root.render('Everything is fine.');
1793 });
1794 });
1795
1796 // Should finish without throwing.
1797 assertConsoleErrorDev([
1798 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' +
1799 '\n in <stack>',
1800 ]);
1801 expect(root).toMatchRenderedOutput('Everything is fine.');
1802 });
1803
1804 it('uncaught errors are discarded if the render is aborted, case 2', async () => {
1805 const {useState} = React;
1806 const root = ReactNoop.createRoot();
1807
1808 let setShouldThrow;
1809 function Oops() {
1810 const [shouldThrow, _setShouldThrow] = useState(false);
1811 setShouldThrow = _setShouldThrow;
1812 if (shouldThrow) {
1813 throw Error('Oops');
1814 }
1815 return null;
1816 }
1817
1818 function AllGood() {
1819 Scheduler.log('Everything is fine.');
1820 return 'Everything is fine.';
1821 }
1822
1823 await act(() => {
1824 root.render(<Oops />);
1825 });
1826
1827 await act(async () => {
1828 // Schedule a default pri and a low pri update on the root.
1829 root.render(<Oops />);
1830 React.startTransition(() => {
1831 root.render(<AllGood />);
1832 });
1833
1834 // Render through just the default pri update. The low pri update remains on
1835 // the queue.
1836 await waitFor(['Everything is fine.']);
1837
1838 // Schedule a discrete update on a child that triggers an error.
1839 // The root should capture this error. But since there's still a pending
1840 // update on the root, the error should be suppressed.
1841 ReactNoop.discreteUpdates(() => {
1842 setShouldThrow(true);
1843 });
1844 });
1845 // Should render the final state without throwing the error.
1846 assertLog(['Everything is fine.']);
1847 assertConsoleErrorDev([
1848 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' +
1849 '\n in <stack>',
1850 ]);
1851 expect(root).toMatchRenderedOutput('Everything is fine.');
1852 });
1853
1854 it("does not infinite loop if there's a render phase update in the same render as an error", async () => {
1855 // Some React features may schedule a render phase update as an
1856 // implementation detail. When an error is accompanied by a render phase
1857 // update, we assume that it comes from React internals, because render
1858 // phase updates triggered from userspace are not allowed (we log a
1859 // warning). So we keep attempting to recover until no more opaque
1860 // identifiers need to be upgraded. However, we should give up after some
1861 // point to prevent an infinite loop in the case where there is (by
1862 // accident) a render phase triggered from userspace.
1863
1864 spyOnDev(console, 'error').mockImplementation(() => {});
1865 spyOnDev(console, 'warn').mockImplementation(() => {});
1866
1867 let numberOfThrows = 0;
1868
1869 let setStateInRenderPhase;
1870 function Child() {
1871 const [, setState] = React.useState(0);
1872 setStateInRenderPhase = setState;
1873 return 'All good';
1874 }
1875
1876 function App({shouldThrow}) {
1877 if (shouldThrow) {
1878 setStateInRenderPhase();
1879 numberOfThrows++;
1880 throw new Error('Oops!');
1881 }
1882 return <Child />;
1883 }
1884
1885 const root = ReactNoop.createRoot();
1886 await act(() => {
1887 root.render(<App shouldThrow={false} />);
1888 });
1889 expect(root).toMatchRenderedOutput('All good');
1890
1891 let error;
1892 try {
1893 await act(() => {
1894 root.render(<App shouldThrow={true} />);
1895 });
1896 } catch (e) {
1897 error = e;
1898 }
1899
1900 expect(error.message).toBe('Oops!');
1901 expect(numberOfThrows < 100).toBe(true);
1902
1903 if (__DEV__) {
1904 expect(console.error).toHaveBeenCalledTimes(1);
1905 expect(console.error.mock.calls[0][0]).toContain(
1906 'Cannot update a component (`%s`) while rendering a different component',
1907 );
1908 expect(console.warn).toHaveBeenCalledTimes(1);
1909 expect(console.warn.mock.calls[0][1]).toContain(
1910 'An error occurred in the <App> component',
1911 );
1912 }
1913 });
1914
1915 if (global.__PERSISTENT__) {
1916 it('regression test: should fatal if error is thrown at the root', async () => {
1917 const root = ReactNoop.createRoot();
1918 root.render('Error when completing root');
1919 await waitForThrow('Error when completing root');
1920 });
1921 }
1922 });