@samitouri / QOS-React-2 / commits / 4f29ba1cc5

support errorInfo in onRecoverableError (#24591)

* extend onRecoverableError API to support errorInfo errorInfo has been used in Error Boundaries wiht componentDidCatch for a while now. To date this metadata only contained a componentStack. onRecoverableError only receives an error (type mixed) argument and thus providing additional error metadata was not possible without mutating user created mixed objects. This change modifies rootConcurrentErrors rootRecoverableErrors, and hydrationErrors so all expect CapturedValue types. additionally a new factory function allows the creation of CapturedValues from a value plus a hash and stack. In general, client derived CapturedValues will be created using the original function which derives a componentStack from a fiber and server originated CapturedValues will be created using with a passed in hash and optional componentStack.

Josh Story committed Jun 6, 2022 at 17:23 UTC 4f29ba1cc52061e439cede3813e100557b23a15c
22 files changed +495 -267
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+119 -72
@@ -90,46 +90,28 @@ describe('ReactDOMFizzServer', () => {
90 });
91
92 function expectErrors(errorsArr, toBeDevArr, toBeProdArr) {
93 - const mappedErrows = errorsArr.map(error => {
94 - if (error.componentStack) {
95 - return [
96 - error.message,
97 - error.hash,
98 - normalizeCodeLocInfo(error.componentStack),
99 - ];
100 - } else if (error.hash) {
101 - return [error.message, error.hash];
93 + const mappedErrows = errorsArr.map(({error, errorInfo}) => {
94 + const stack = errorInfo && errorInfo.componentStack;
95 + const digest = errorInfo && errorInfo.digest;
96 + if (stack) {
97 + return [error.message, digest, normalizeCodeLocInfo(stack)];
98 + } else if (digest) {
99 + return [error.message, digest];
100 }
101 return error.message;
102 });
103 if (__DEV__) {
106 - expect(mappedErrows).toEqual(
107 - toBeDevArr,
108 - // .map(([errorMessage, errorHash, errorComponentStack]) => {
109 - // if (typeof error === 'string' || error instanceof String) {
110 - // return error;
111 - // }
112 - // let str = JSON.stringify(error).replace(/\\n/g, '\n');
113 - // // this gets stripped away by normalizeCodeLocInfo...
114 - // // Kind of hacky but lets strip it away here too just so they match...
115 - // // easier than fixing the regex to account for this edge case
116 - // if (str.endsWith('at **)"}')) {
117 - // str = str.replace(/at \*\*\)\"}$/, 'at **)');
118 - // }
119 - // return str;
120 - // }),
121 - );
104 + expect(mappedErrows).toEqual(toBeDevArr);
105 } else {
106 expect(mappedErrows).toEqual(toBeProdArr);
107 }
108 }
109
127 - // @TODO we will use this in a followup change once we start exposing componentStacks from server errors
128 - // function componentStack(components) {
129 - // return components
130 - // .map(component => `\n in ${component} (at **)`)
131 - // .join('');
132 - // }
110 + function componentStack(components) {
111 + return components
112 + .map(component => `\n in ${component} (at **)`)
113 + .join('');
114 + }
115
116 async function act(callback) {
117 await callback();
@@ -471,8 +453,8 @@ describe('ReactDOMFizzServer', () => {
453 bootstrapped = true;
454 // Attempt to hydrate the content.
455 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
474 - onRecoverableError(error) {
475 - errors.push(error);
456 + onRecoverableError(error, errorInfo) {
457 + errors.push({error, errorInfo});
458 },
459 });
460 };
@@ -483,8 +465,8 @@ describe('ReactDOMFizzServer', () => {
465 loggedErrors.push(x);
466 return 'Hash of (' + x.message + ')';
467 }
486 - // const expectedHash = onError(theError);
487 - // loggedErrors.length = 0;
468 + const expectedDigest = onError(theError);
469 + loggedErrors.length = 0;
470
471 await act(async () => {
472 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
@@ -519,9 +501,18 @@ describe('ReactDOMFizzServer', () => {
501 expect(Scheduler).toFlushAndYield([]);
502 expectErrors(
503 errors,
522 - [theError.message],
504 [
524 - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
505 + [
506 + theError.message,
507 + expectedDigest,
508 + componentStack(['Lazy', 'Suspense', 'div', 'App']),
509 + ],
510 + ],
511 + [
512 + [
513 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
514 + expectedDigest,
515 + ],
516 ],
517 );
518
@@ -577,8 +568,8 @@ describe('ReactDOMFizzServer', () => {
568 loggedErrors.push(x);
569 return 'hash of (' + x.message + ')';
570 }
580 - // const expectedHash = onError(theError);
581 - // loggedErrors.length = 0;
571 + const expectedDigest = onError(theError);
572 + loggedErrors.length = 0;
573
574 function App({isClient}) {
575 return (
@@ -605,8 +596,8 @@ describe('ReactDOMFizzServer', () => {
596 const errors = [];
597 // Attempt to hydrate the content.
598 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
608 - onRecoverableError(error) {
609 - errors.push(error);
599 + onRecoverableError(error, errorInfo) {
600 + errors.push({error, errorInfo});
601 },
602 });
603 Scheduler.unstable_flushAll();
@@ -630,9 +621,18 @@ describe('ReactDOMFizzServer', () => {
621
622 expectErrors(
623 errors,
633 - [theError.message],
624 [
635 - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
625 + [
626 + theError.message,
627 + expectedDigest,
628 + componentStack(['Suspense', 'div', 'App']),
629 + ],
630 + ],
631 + [
632 + [
633 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
634 + expectedDigest,
635 + ],
636 ],
637 );
638
@@ -675,8 +675,8 @@ describe('ReactDOMFizzServer', () => {
675 loggedErrors.push(x);
676 return 'hash(' + x.message + ')';
677 }
678 - // const expectedHash = onError(theError);
679 - // loggedErrors.length = 0;
678 + const expectedDigest = onError(theError);
679 + loggedErrors.length = 0;
680
681 await act(async () => {
682 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
@@ -693,8 +693,8 @@ describe('ReactDOMFizzServer', () => {
693 const errors = [];
694 // Attempt to hydrate the content.
695 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
696 - onRecoverableError(error) {
697 - errors.push(error);
696 + onRecoverableError(error, errorInfo) {
697 + errors.push({error, errorInfo});
698 },
699 });
700 Scheduler.unstable_flushAll();
@@ -703,9 +703,18 @@ describe('ReactDOMFizzServer', () => {
703
704 expectErrors(
705 errors,
706 - [theError.message],
706 [
708 - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
707 + [
708 + theError.message,
709 + expectedDigest,
710 + componentStack(['Erroring', 'Suspense', 'div', 'App']),
711 + ],
712 + ],
713 + [
714 + [
715 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
716 + expectedDigest,
717 + ],
718 ],
719 );
720 });
@@ -735,8 +744,8 @@ describe('ReactDOMFizzServer', () => {
744 loggedErrors.push(x);
745 return 'hash(' + x.message + ')';
746 }
738 - // const expectedHash = onError(theError);
739 - // loggedErrors.length = 0;
747 + const expectedDigest = onError(theError);
748 + loggedErrors.length = 0;
749
750 await act(async () => {
751 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
@@ -753,8 +762,8 @@ describe('ReactDOMFizzServer', () => {
762 const errors = [];
763 // Attempt to hydrate the content.
764 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
756 - onRecoverableError(error) {
757 - errors.push(error);
765 + onRecoverableError(error, errorInfo) {
766 + errors.push({error, errorInfo});
767 },
768 });
769 Scheduler.unstable_flushAll();
@@ -773,9 +782,18 @@ describe('ReactDOMFizzServer', () => {
782
783 expectErrors(
784 errors,
776 - [theError.message],
785 [
778 - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
786 + [
787 + theError.message,
788 + expectedDigest,
789 + componentStack(['Lazy', 'Suspense', 'div', 'App']),
790 + ],
791 + ],
792 + [
793 + [
794 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
795 + expectedDigest,
796 + ],
797 ],
798 );
799
@@ -1053,9 +1071,10 @@ describe('ReactDOMFizzServer', () => {
1071 }
1072
1073 const loggedErrors = [];
1074 + const expectedDigest = 'Hash for Abort';
1075 function onError(error) {
1076 loggedErrors.push(error);
1058 - return `Hash of (${error.message})`;
1077 + return expectedDigest;
1078 }
1079
1080 let controls;
@@ -1069,8 +1088,8 @@ describe('ReactDOMFizzServer', () => {
1088 const errors = [];
1089 // Attempt to hydrate the content.
1090 ReactDOMClient.hydrateRoot(container, <App />, {
1072 - onRecoverableError(error) {
1073 - errors.push(error);
1091 + onRecoverableError(error, errorInfo) {
1092 + errors.push({error, errorInfo});
1093 },
1094 });
1095 Scheduler.unstable_flushAll();
@@ -1087,9 +1106,12 @@ describe('ReactDOMFizzServer', () => {
1106 expect(Scheduler).toFlushAndYield([]);
1107 expectErrors(
1108 errors,
1090 - ['This Suspense boundary was aborted by the server'],
1109 + [['This Suspense boundary was aborted by the server.', expectedDigest]],
1110 [
1092 - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1111 + [
1112 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1113 + expectedDigest,
1114 + ],
1115 ],
1116 );
1117 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
@@ -1755,8 +1777,8 @@ describe('ReactDOMFizzServer', () => {
1777 loggedErrors.push(x);
1778 return `hash of (${x.message})`;
1779 }
1758 - // const expectedHash = onError(theError);
1759 - // loggedErrors.length = 0;
1780 + const expectedDigest = onError(theError);
1781 + loggedErrors.length = 0;
1782
1783 let controls;
1784 await act(async () => {
@@ -1775,8 +1797,8 @@ describe('ReactDOMFizzServer', () => {
1797 const errors = [];
1798 // Attempt to hydrate the content.
1799 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
1778 - onRecoverableError(error) {
1779 - errors.push(error);
1800 + onRecoverableError(error, errorInfo) {
1801 + errors.push({error, errorInfo});
1802 },
1803 });
1804 Scheduler.unstable_flushAll();
@@ -1809,9 +1831,25 @@ describe('ReactDOMFizzServer', () => {
1831 expect(Scheduler).toFlushAndYield([]);
1832 expectErrors(
1833 errors,
1812 - [theError.message],
1834 [
1814 - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1835 + [
1836 + theError.message,
1837 + expectedDigest,
1838 + componentStack([
1839 + 'AsyncText',
1840 + 'h1',
1841 + 'Suspense',
1842 + 'div',
1843 + 'Suspense',
1844 + 'App',
1845 + ]),
1846 + ],
1847 + ],
1848 + [
1849 + [
1850 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1851 + expectedDigest,
1852 + ],
1853 ],
1854 );
1855
@@ -3142,8 +3180,8 @@ describe('ReactDOMFizzServer', () => {
3180 loggedErrors.push(x);
3181 return x.message.replace('bad message', 'bad hash');
3182 }
3145 - // const expectedHash = onError(theError);
3146 - // loggedErrors.length = 0;
3183 + const expectedDigest = onError(theError);
3184 + loggedErrors.length = 0;
3185
3186 await act(async () => {
3187 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
@@ -3156,8 +3194,8 @@ describe('ReactDOMFizzServer', () => {
3194
3195 const errors = [];
3196 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
3159 - onRecoverableError(error) {
3160 - errors.push(error);
3197 + onRecoverableError(error, errorInfo) {
3198 + errors.push({error, errorInfo});
3199 },
3200 });
3201 expect(Scheduler).toFlushAndYield([]);
@@ -3165,9 +3203,18 @@ describe('ReactDOMFizzServer', () => {
3203 // If escaping were not done we would get a message that says "bad hash"
3204 expectErrors(
3205 errors,
3168 - [theError.message],
3206 [
3170 - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
3207 + [
3208 + theError.message,
3209 + expectedDigest,
3210 + componentStack(['Erroring', 'Suspense', 'div', 'App']),
3211 + ],
3212 + ],
3213 + [
3214 + [
3215 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
3216 + expectedDigest,
3217 + ],
3218 ],
3219 );
3220 });
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+2 -2
@@ -215,7 +215,7 @@ describe('ReactDOMFizzServer', () => {
215 expect(result).toContain('Loading');
216
217 expect(errors).toEqual([
218 - 'This Suspense boundary was aborted by the server',
218 + 'This Suspense boundary was aborted by the server.',
219 ]);
220 });
221
@@ -256,7 +256,7 @@ describe('ReactDOMFizzServer', () => {
256 reader.cancel();
257
258 expect(errors).toEqual([
259 - 'This Suspense boundary was aborted by the server',
259 + 'This Suspense boundary was aborted by the server.',
260 ]);
261
262 hasLoaded = true;
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+5 -5
@@ -226,7 +226,7 @@ describe('ReactDOMFizzServer', () => {
226 expect(output.result).toBe('');
227 expect(reportedErrors).toEqual([
228 theError.message,
229 - 'This Suspense boundary was aborted by the server',
229 + 'This Suspense boundary was aborted by the server.',
230 ]);
231 expect(reportedShellErrors).toEqual([theError]);
232 });
@@ -322,7 +322,7 @@ describe('ReactDOMFizzServer', () => {
322 await completed;
323
324 expect(errors).toEqual([
325 - 'This Suspense boundary was aborted by the server',
325 + 'This Suspense boundary was aborted by the server.',
326 ]);
327 expect(output.error).toBe(undefined);
328 expect(output.result).toContain('Loading');
@@ -365,8 +365,8 @@ describe('ReactDOMFizzServer', () => {
365
366 expect(errors).toEqual([
367 // There are two boundaries that abort
368 - 'This Suspense boundary was aborted by the server',
369 - 'This Suspense boundary was aborted by the server',
368 + 'This Suspense boundary was aborted by the server.',
369 + 'This Suspense boundary was aborted by the server.',
370 ]);
371 expect(output.error).toBe(undefined);
372 expect(output.result).toContain('Loading');
@@ -603,7 +603,7 @@ describe('ReactDOMFizzServer', () => {
603 await completed;
604
605 expect(errors).toEqual([
606 - 'This Suspense boundary was aborted by the server',
606 + 'This Suspense boundary was aborted by the server.',
607 ]);
608 expect(rendered).toBe(false);
609 expect(isComplete).toBe(true);
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+41 -11
@@ -11,6 +11,7 @@ let React;
11 let ReactDOMClient;
12 let ReactDOMServer;
13 let act;
14 +let usingPartialRenderer;
15
16 const util = require('util');
17 const realConsoleError = console.error;
@@ -25,6 +26,8 @@ describe('ReactDOMServerHydration', () => {
26 ReactDOMServer = require('react-dom/server');
27 act = require('react-dom/test-utils').act;
28
29 + usingPartialRenderer = global.__WWW__ && !__EXPERIMENTAL__;
30 +
31 console.error = jest.fn();
32 container = document.createElement('div');
33 document.body.appendChild(container);
@@ -727,9 +730,16 @@ describe('ReactDOMServerHydration', () => {
730 </div>
731 );
732 }
733 +
734 + // @TODO FB bundles use a different renderer that does not serialize errors to the client
735 + const mismatchEl = usingPartialRenderer ? '<p>' : '<template>';
736 + // @TODO changes made to sending Fizz errors to client led to the insertion of templates in client rendered
737 + // suspense boundaries. This leaks in this test becuase the client rendered suspense boundary appears like
738 + // unhydrated tail nodes and this template is the first match. When we add special case handling for client
739 + // rendered suspense boundaries this test will likely change again
740 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
741 Array [
732 - "Warning: Did not expect server HTML to contain a <p> in <div>.
742 + "Warning: Did not expect server HTML to contain a ${mismatchEl} in <div>.
743 in div (at **)
744 in Mismatch (at **)",
745 "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
@@ -809,11 +819,21 @@ describe('ReactDOMServerHydration', () => {
819 </div>
820 );
821 }
812 - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
813 - Array [
814 - "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
815 - ]
816 - `);
822 + // We gate this assertion becuase fb-classic uses PartialRenderer for renderToString and it does not
823 + // serialize server errors and send to client
824 + if (usingPartialRenderer) {
825 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
826 + Array [
827 + "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
828 + ]
829 + `);
830 + } else {
831 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
832 + Array [
833 + "Caught [This Suspense boundary was aborted by the server.]",
834 + ]
835 + `);
836 + }
837 });
838
839 // @gate __DEV__
@@ -834,11 +854,21 @@ describe('ReactDOMServerHydration', () => {
854 </div>
855 );
856 }
837 - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
838 - Array [
839 - "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
840 - ]
841 - `);
857 + // We gate this assertion becuase fb-classic uses PartialRenderer for renderToString and it does not
858 + // serialize server errors and send to client
859 + if (usingPartialRenderer) {
860 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
861 + Array [
862 + "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
863 + ]
864 + `);
865 + } else {
866 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
867 + Array [
868 + "Caught [This Suspense boundary was aborted by the server.]",
869 + ]
870 + `);
871 + }
872 });
873 });
874
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+53 -20
@@ -19,6 +19,7 @@ let Suspense;
19 let SuspenseList;
20 let act;
21 let IdleEventPriority;
22 +let usingPartialRenderer;
23
24 function normalizeCodeLocInfo(strOrErr) {
25 if (strOrErr && strOrErr.replace) {
@@ -110,6 +111,8 @@ describe('ReactDOMServerPartialHydration', () => {
111 SuspenseList = React.SuspenseList;
112 }
113
114 + usingPartialRenderer = global.__WWW__ && !__EXPERIMENTAL__;
115 +
116 IdleEventPriority = require('react-reconciler/constants').IdleEventPriority;
117 });
118
@@ -1668,10 +1671,16 @@ describe('ReactDOMServerPartialHydration', () => {
1671 Scheduler.unstable_yieldValue(error.message);
1672 },
1673 });
1671 - expect(Scheduler).toFlushAndYield([
1672 - 'The server could not finish this Suspense boundary, likely due to ' +
1673 - 'an error during server rendering. Switched to client rendering.',
1674 - ]);
1674 + // we exclude fb bundles with partial renderer
1675 + if (__DEV__ && !usingPartialRenderer) {
1676 + expect(Scheduler).toFlushAndYield([
1677 + 'This Suspense boundary was aborted by the server.',
1678 + ]);
1679 + } else {
1680 + expect(Scheduler).toFlushAndYield([
1681 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1682 + ]);
1683 + }
1684 jest.runAllTimers();
1685
1686 expect(container.textContent).toBe('Hello');
@@ -1730,10 +1739,16 @@ describe('ReactDOMServerPartialHydration', () => {
1739 Scheduler.unstable_yieldValue(error.message);
1740 },
1741 });
1733 - expect(Scheduler).toFlushAndYield([
1734 - 'The server could not finish this Suspense boundary, likely due to ' +
1735 - 'an error during server rendering. Switched to client rendering.',
1736 - ]);
1742 + // we exclude fb bundles with partial renderer
1743 + if (__DEV__ && !usingPartialRenderer) {
1744 + expect(Scheduler).toFlushAndYield([
1745 + 'This Suspense boundary was aborted by the server.',
1746 + ]);
1747 + } else {
1748 + expect(Scheduler).toFlushAndYield([
1749 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1750 + ]);
1751 + }
1752 // This will have exceeded the suspended time so we should timeout.
1753 jest.advanceTimersByTime(500);
1754 // The boundary should longer be suspended for the middle content
@@ -1797,10 +1812,16 @@ describe('ReactDOMServerPartialHydration', () => {
1812 Scheduler.unstable_yieldValue(error.message);
1813 },
1814 });
1800 - expect(Scheduler).toFlushAndYield([
1801 - 'The server could not finish this Suspense boundary, likely due to ' +
1802 - 'an error during server rendering. Switched to client rendering.',
1803 - ]);
1815 + // we exclude fb bundles with partial renderer
1816 + if (__DEV__ && !usingPartialRenderer) {
1817 + expect(Scheduler).toFlushAndYield([
1818 + 'This Suspense boundary was aborted by the server.',
1819 + ]);
1820 + } else {
1821 + expect(Scheduler).toFlushAndYield([
1822 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1823 + ]);
1824 + }
1825 // This will have exceeded the suspended time so we should timeout.
1826 jest.advanceTimersByTime(500);
1827 // The boundary should longer be suspended for the middle content
@@ -2115,10 +2136,16 @@ describe('ReactDOMServerPartialHydration', () => {
2136 });
2137
2138 suspend = true;
2118 - expect(Scheduler).toFlushAndYield([
2119 - 'The server could not finish this Suspense boundary, likely due to ' +
2120 - 'an error during server rendering. Switched to client rendering.',
2121 - ]);
2139 + // we exclude fb bundles with partial renderer
2140 + if (__DEV__ && !usingPartialRenderer) {
2141 + expect(Scheduler).toFlushAndYield([
2142 + 'This Suspense boundary was aborted by the server.',
2143 + ]);
2144 + } else {
2145 + expect(Scheduler).toFlushAndYield([
2146 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
2147 + ]);
2148 + }
2149
2150 // We haven't hydrated the second child but the placeholder is still in the list.
2151 expect(container.textContent).toBe('ALoading B');
@@ -2178,10 +2205,16 @@ describe('ReactDOMServerPartialHydration', () => {
2205 Scheduler.unstable_yieldValue(error.message);
2206 },
2207 });
2181 - expect(Scheduler).toFlushAndYield([
2182 - 'The server could not finish this Suspense boundary, likely due to ' +
2183 - 'an error during server rendering. Switched to client rendering.',
2184 - ]);
2208 + // we exclude fb bundles with partial renderer
2209 + if (__DEV__ && !usingPartialRenderer) {
2210 + expect(Scheduler).toFlushAndYield([
2211 + 'This Suspense boundary was aborted by the server.',
2212 + ]);
2213 + } else {
2214 + expect(Scheduler).toFlushAndYield([
2215 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
2216 + ]);
2217 + }
2218 jest.runAllTimers();
2219
2220 expect(ref.current).toBe(span);
packages/react-dom/src/client/ReactDOMHostConfig.js
+35 -20
@@ -729,29 +729,44 @@ export function isSuspenseInstancePending(instance: SuspenseInstance) {
729 export function isSuspenseInstanceFallback(instance: SuspenseInstance) {
730 return instance.data === SUSPENSE_FALLBACK_START_DATA;
731 }
732 +
733 export function getSuspenseInstanceFallbackErrorDetails(
734 instance: SuspenseInstance,
734 -) {
735 - const nextSibling = instance.nextSibling;
736 - let errorMessage /*, errorComponentStack, errorHash*/;
737 - if (
738 - nextSibling &&
739 - nextSibling.nodeType === ELEMENT_NODE &&
740 - nextSibling.nodeName.toLowerCase() === 'template'
741 - ) {
742 - const msg = ((nextSibling: any): HTMLTemplateElement).dataset.msg;
743 - if (msg !== null) errorMessage = msg;
744 -
745 - // @TODO read and return hash and componentStack once we know how we are goign to
746 - // expose this extra errorInfo to onRecoverableError
747 -
748 - // const hash = ((nextSibling: any): HTMLTemplateElement).dataset.hash;
749 - // if (hash !== null) errorHash = hash;
750 -
751 - // const stack = ((nextSibling: any): HTMLTemplateElement).dataset.stack;
752 - // if (stack !== null) errorComponentStack = stack;
735 +): {digest: ?string, message?: string, stack?: string} {
736 + const dataset =
737 + instance.nextSibling && ((instance.nextSibling: any): HTMLElement).dataset;
738 + let digest, message, stack;
739 + if (dataset) {
740 + digest = dataset.dgst;
741 + if (__DEV__) {
742 + message = dataset.msg;
743 + stack = dataset.stck;
744 + }
745 + }
746 + if (__DEV__) {
747 + return {
748 + message,
749 + digest,
750 + stack,
751 + };
752 + } else {
753 + // Object gets DCE'd if constructed in tail position and matches callsite destructuring
754 + return {
755 + digest,
756 + };
757 }
754 - return {errorMessage /*, errorComponentStack, errorHash*/};
758 +
759 + // let value = {message: undefined, hash: undefined};
760 + // const nextSibling = instance.nextSibling;
761 + // if (nextSibling) {
762 + // const dataset = ((nextSibling: any): HTMLTemplateElement).dataset;
763 + // value.message = dataset.msg;
764 + // value.hash = dataset.hash;
765 + // if (__DEV__) {
766 + // value.stack = dataset.stack;
767 + // }
768 + // }
769 + // return value;
770 }
771
772 export function registerSuspenseInstanceRetry(
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+47 -31
@@ -1527,16 +1527,22 @@ const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(
1527 const endSuspenseBoundary = stringToPrecomputedChunk('<!--/$-->');
1528
1529 const clientRenderedSuspenseBoundaryError1 = stringToPrecomputedChunk(
1530 - '<template data-hash="',
1530 + '<template',
1531 +);
1532 +const clientRenderedSuspenseBoundaryErrorAttrInterstitial = stringToPrecomputedChunk(
1533 + '"',
1534 );
1535 const clientRenderedSuspenseBoundaryError1A = stringToPrecomputedChunk(
1533 - '" data-msg="',
1536 + ' data-dgst="',
1537 );
1538 const clientRenderedSuspenseBoundaryError1B = stringToPrecomputedChunk(
1536 - '" data-stack="',
1539 + ' data-msg="',
1540 +);
1541 +const clientRenderedSuspenseBoundaryError1C = stringToPrecomputedChunk(
1542 + ' data-stck="',
1543 );
1544 const clientRenderedSuspenseBoundaryError2 = stringToPrecomputedChunk(
1539 - '"></template>',
1545 + '></template>',
1546 );
1547
1548 export function pushStartCompletedSuspenseBoundary(
@@ -1576,7 +1582,7 @@ export function writeStartPendingSuspenseBoundary(
1582 export function writeStartClientRenderedSuspenseBoundary(
1583 destination: Destination,
1584 responseState: ResponseState,
1579 - errorHash: ?string,
1585 + errorDigest: ?string,
1586 errorMesssage: ?string,
1587 errorComponentStack: ?string,
1588 ): boolean {
@@ -1585,33 +1591,43 @@ export function writeStartClientRenderedSuspenseBoundary(
1591 destination,
1592 startClientRenderedSuspenseBoundary,
1593 );
1588 - if (errorHash) {
1589 - writeChunk(destination, clientRenderedSuspenseBoundaryError1);
1590 - writeChunk(destination, stringToChunk(escapeTextForBrowser(errorHash)));
1591 - // In prod errorMessage will usually be nullish but there is one case where
1592 - // it is used (currently when the server aborts the task) so we leave it ungated.
1594 + writeChunk(destination, clientRenderedSuspenseBoundaryError1);
1595 + if (errorDigest) {
1596 + writeChunk(destination, clientRenderedSuspenseBoundaryError1A);
1597 + writeChunk(destination, stringToChunk(escapeTextForBrowser(errorDigest)));
1598 + writeChunk(
1599 + destination,
1600 + clientRenderedSuspenseBoundaryErrorAttrInterstitial,
1601 + );
1602 + }
1603 + if (__DEV__) {
1604 if (errorMesssage) {
1594 - writeChunk(destination, clientRenderedSuspenseBoundaryError1A);
1605 + writeChunk(destination, clientRenderedSuspenseBoundaryError1B);
1606 writeChunk(
1607 destination,
1608 stringToChunk(escapeTextForBrowser(errorMesssage)),
1609 );
1610 + writeChunk(
1611 + destination,
1612 + clientRenderedSuspenseBoundaryErrorAttrInterstitial,
1613 + );
1614 }
1600 - if (__DEV__) {
1601 - // Component stacks are currently only captured in dev
1602 - if (errorComponentStack) {
1603 - writeChunk(destination, clientRenderedSuspenseBoundaryError1B);
1604 - writeChunk(
1605 - destination,
1606 - stringToChunk(escapeTextForBrowser(errorComponentStack)),
1607 - );
1608 - }
1615 + if (errorComponentStack) {
1616 + writeChunk(destination, clientRenderedSuspenseBoundaryError1C);
1617 + writeChunk(
1618 + destination,
1619 + stringToChunk(escapeTextForBrowser(errorComponentStack)),
1620 + );
1621 + writeChunk(
1622 + destination,
1623 + clientRenderedSuspenseBoundaryErrorAttrInterstitial,
1624 + );
1625 }
1610 - result = writeChunkAndReturn(
1611 - destination,
1612 - clientRenderedSuspenseBoundaryError2,
1613 - );
1626 }
1627 + result = writeChunkAndReturn(
1628 + destination,
1629 + clientRenderedSuspenseBoundaryError2,
1630 + );
1631 return result;
1632 }
1633 export function writeEndCompletedSuspenseBoundary(
@@ -1772,7 +1788,7 @@ export function writeEndSegment(
1788 // const SUSPENSE_PENDING_START_DATA = '$?';
1789 // const SUSPENSE_FALLBACK_START_DATA = '$!';
1790 //
1775 -// function clientRenderBoundary(suspenseBoundaryID, errorHash, errorMsg, errorComponentStack) {
1791 +// function clientRenderBoundary(suspenseBoundaryID, errorDigest, errorMsg, errorComponentStack) {
1792 // // Find the fallback's first element.
1793 // const suspenseIdNode = document.getElementById(suspenseBoundaryID);
1794 // if (!suspenseIdNode) {
@@ -1786,9 +1802,9 @@ export function writeEndSegment(
1802 // suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
1803 // // assign error metadata to first sibling
1804 // let dataset = suspenseIdNode.dataset;
1789 -// if (errorHash) dataset.hash = errorHash;
1805 +// if (errorDigest) dataset.dgst = errorDigest;
1806 // if (errorMsg) dataset.msg = errorMsg;
1791 -// if (errorComponentStack) dataset.stack = errorComponentStack;
1807 +// if (errorComponentStack) dataset.stck = errorComponentStack;
1808 // // Tell React to retry it if the parent already hydrated.
1809 // if (suspenseNode._reactRetry) {
1810 // suspenseNode._reactRetry();
@@ -1876,7 +1892,7 @@ const completeSegmentFunction =
1892 const completeBoundaryFunction =
1893 'function $RC(a,b){a=document.getElementById(a);b=document.getElementById(b);b.parentNode.removeChild(b);if(a){a=a.previousSibling;var f=a.parentNode,c=a.nextSibling,e=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d)if(0===e)break;else e--;else"$"!==d&&"$?"!==d&&"$!"!==d||e++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;b.firstChild;)f.insertBefore(b.firstChild,c);a.data="$";a._reactRetry&&a._reactRetry()}}';
1894 const clientRenderFunction =
1879 - 'function $RX(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.hash=c),d&&(a.msg=d),e&&(a.stack=e),b._reactRetry&&b._reactRetry())}';
1895 + 'function $RX(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),b._reactRetry&&b._reactRetry())}';
1896
1897 const completeSegmentScript1Full = stringToPrecomputedChunk(
1898 completeSegmentFunction + ';$RS("',
@@ -1957,7 +1973,7 @@ export function writeClientRenderBoundaryInstruction(
1973 destination: Destination,
1974 responseState: ResponseState,
1975 boundaryID: SuspenseBoundaryID,
1960 - errorHash: ?string,
1976 + errorDigest: ?string,
1977 errorMessage?: string,
1978 errorComponentStack?: string,
1979 ): boolean {
@@ -1979,11 +1995,11 @@ export function writeClientRenderBoundaryInstruction(
1995
1996 writeChunk(destination, boundaryID);
1997 writeChunk(destination, clientRenderScript1A);
1982 - if (errorHash || errorMessage || errorComponentStack) {
1998 + if (errorDigest || errorMessage || errorComponentStack) {
1999 writeChunk(destination, clientRenderErrorScriptArgInterstitial);
2000 writeChunk(
2001 destination,
1986 - stringToChunk(escapeJSStringsForInstructionScripts(errorHash || '')),
2002 + stringToChunk(escapeJSStringsForInstructionScripts(errorDigest || '')),
2003 );
2004 }
2005 if (errorMessage || errorComponentStack) {
packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
+6 -3
@@ -149,9 +149,9 @@ export function writeStartClientRenderedSuspenseBoundary(
149 destination: Destination,
150 responseState: ResponseState,
151 // flushing these error arguments are not currently supported in this legacy streaming format.
152 - errorHash: ?string,
153 - errorMessage?: string,
154 - errorComponentStack?: string,
152 + errorDigest: ?string,
153 + errorMessage: ?string,
154 + errorComponentStack: ?string,
155 ): boolean {
156 if (responseState.generateStaticMarkup) {
157 // A client rendered boundary is done and doesn't need a representation in the HTML
@@ -161,6 +161,9 @@ export function writeStartClientRenderedSuspenseBoundary(
161 return writeStartClientRenderedSuspenseBoundaryImpl(
162 destination,
163 responseState,
164 + errorDigest,
165 + errorMessage,
166 + errorComponentStack,
167 );
168 }
169 export function writeEndCompletedSuspenseBoundary(
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+2 -2
@@ -226,7 +226,7 @@ export function writeStartClientRenderedSuspenseBoundary(
226 destination: Destination,
227 responseState: ResponseState,
228 // TODO: encode error for native
229 - errorHash: ?string,
229 + errorDigest: ?string,
230 errorMessage: ?string,
231 errorComponentStack: ?string,
232 ): boolean {
@@ -300,7 +300,7 @@ export function writeClientRenderBoundaryInstruction(
300 responseState: ResponseState,
301 boundaryID: SuspenseBoundaryID,
302 // TODO: encode error for native
303 - errorHash: ?string,
303 + errorDigest: ?string,
304 errorMessage: ?string,
305 errorComponentStack: ?string,
306 ): boolean {
packages/react-reconciler/src/ReactCapturedValue.js
+16 -1
@@ -15,9 +15,10 @@ export type CapturedValue<T> = {|
15 value: T,
16 source: Fiber | null,
17 stack: string | null,
18 + digest: string | null,
19 |};
20
20 -export function createCapturedValue<T>(
21 +export function createCapturedValueAtFiber<T>(
22 value: T,
23 source: Fiber,
24 ): CapturedValue<T> {
@@ -27,5 +28,19 @@ export function createCapturedValue<T>(
28 value,
29 source,
30 stack: getStackByFiberInDevAndProd(source),
31 + digest: null,
32 + };
33 +}
34 +
35 +export function createCapturedValue<T>(
36 + value: T,
37 + digest: ?string,
38 + stack: ?string,
39 +): CapturedValue<T> {
40 + return {
41 + value,
42 + source: null,
43 + stack: stack != null ? stack : null,
44 + digest: digest != null ? digest : null,
45 };
46 }
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+48 -25
@@ -237,7 +237,11 @@ import {
237 import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.new';
238 import {setWorkInProgressVersion} from './ReactMutableSource.new';
239 import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.new';
240 -import {createCapturedValue} from './ReactCapturedValue';
240 +import {
241 + createCapturedValue,
242 + createCapturedValueAtFiber,
243 + type CapturedValue,
244 +} from './ReactCapturedValue';
245 import {createClassErrorUpdate} from './ReactFiberThrow.new';
246 import is from 'shared/objectIs';
247 import {
@@ -1074,7 +1078,7 @@ function updateClassComponent(
1078 // Schedule the error boundary to re-render using updated state
1079 const update = createClassErrorUpdate(
1080 workInProgress,
1077 - createCapturedValue(error, workInProgress),
1081 + createCapturedValueAtFiber(error, workInProgress),
1082 lane,
1083 );
1084 enqueueCapturedUpdate(workInProgress, update);
@@ -1322,10 +1326,13 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1326 if (workInProgress.flags & ForceClientRender) {
1327 // Something errored during a previous attempt to hydrate the shell, so we
1328 // forced a client render.
1325 - const recoverableError = new Error(
1326 - 'There was an error while hydrating. Because the error happened outside ' +
1327 - 'of a Suspense boundary, the entire root will switch to ' +
1328 - 'client rendering.',
1329 + const recoverableError = createCapturedValueAtFiber(
1330 + new Error(
1331 + 'There was an error while hydrating. Because the error happened outside ' +
1332 + 'of a Suspense boundary, the entire root will switch to ' +
1333 + 'client rendering.',
1334 + ),
1335 + workInProgress,
1336 );
1337 return mountHostRootWithoutHydrating(
1338 current,
@@ -1335,9 +1342,12 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1342 recoverableError,
1343 );
1344 } else if (nextChildren !== prevChildren) {
1338 - const recoverableError = new Error(
1339 - 'This root received an early update, before anything was able ' +
1340 - 'hydrate. Switched the entire root to client rendering.',
1345 + const recoverableError = createCapturedValueAtFiber(
1346 + new Error(
1347 + 'This root received an early update, before anything was able ' +
1348 + 'hydrate. Switched the entire root to client rendering.',
1349 + ),
1350 + workInProgress,
1351 );
1352 return mountHostRootWithoutHydrating(
1353 current,
@@ -1400,7 +1410,7 @@ function mountHostRootWithoutHydrating(
1410 workInProgress: Fiber,
1411 nextChildren: ReactNodeList,
1412 renderLanes: Lanes,
1403 - recoverableError: Error,
1413 + recoverableError: CapturedValue<mixed>,
1414 ) {
1415 // Revert to client rendering.
1416 resetHydrationState();
@@ -2429,7 +2439,7 @@ function retrySuspenseComponentWithoutHydrating(
2439 current: Fiber,
2440 workInProgress: Fiber,
2441 renderLanes: Lanes,
2432 - recoverableError: Error | null,
2442 + recoverableError: CapturedValue<mixed> | null,
2443 ) {
2444 // Falling back to client rendering. Because this has performance
2445 // implications, it's considered a recoverable error, even though the user
@@ -2574,22 +2584,29 @@ function updateDehydratedSuspenseComponent(
2584 // This boundary is in a permanent fallback state. In this case, we'll never
2585 // get an update and we'll never be able to hydrate the final content. Let's just try the
2586 // client side render instead.
2577 - const {errorMessage} = getSuspenseInstanceFallbackErrorDetails(
2578 - suspenseInstance,
2579 - );
2580 - const error = errorMessage
2587 + let digest, message, stack;
2588 + if (__DEV__) {
2589 + ({digest, message, stack} = getSuspenseInstanceFallbackErrorDetails(
2590 + suspenseInstance,
2591 + ));
2592 + } else {
2593 + ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
2594 + }
2595 +
2596 + const error = message
2597 ? // eslint-disable-next-line react-internal/prod-error-codes
2582 - new Error(errorMessage)
2598 + new Error(message)
2599 : new Error(
2600 'The server could not finish this Suspense boundary, likely ' +
2601 'due to an error during server rendering. Switched to ' +
2602 'client rendering.',
2603 );
2604 + const capturedValue = createCapturedValue(error, digest, stack);
2605 return retrySuspenseComponentWithoutHydrating(
2606 current,
2607 workInProgress,
2608 renderLanes,
2592 - error,
2609 + capturedValue,
2610 );
2611 }
2612
@@ -2650,10 +2667,7 @@ function updateDehydratedSuspenseComponent(
2667 // skip hydration.
2668 // Delay having to do this as long as the suspense timeout allows us.
2669 renderDidSuspendDelayIfPossible();
2653 - return retrySuspenseComponentWithoutHydrating(
2654 - current,
2655 - workInProgress,
2656 - renderLanes,
2670 + const capturedValue = createCapturedValue(
2671 new Error(
2672 'This Suspense boundary received an update before it finished ' +
2673 'hydrating. This caused the boundary to switch to client rendering. ' +
@@ -2661,6 +2675,12 @@ function updateDehydratedSuspenseComponent(
2675 'in startTransition.',
2676 ),
2677 );
2678 + return retrySuspenseComponentWithoutHydrating(
2679 + current,
2680 + workInProgress,
2681 + renderLanes,
2682 + capturedValue,
2683 + );
2684 } else if (isSuspenseInstancePending(suspenseInstance)) {
2685 // This component is still pending more data from the server, so we can't hydrate its
2686 // content. We treat it as if this component suspended itself. It might seem as if
@@ -2707,15 +2727,18 @@ function updateDehydratedSuspenseComponent(
2727 if (workInProgress.flags & ForceClientRender) {
2728 // Something errored during hydration. Try again without hydrating.
2729 workInProgress.flags &= ~ForceClientRender;
2710 - return retrySuspenseComponentWithoutHydrating(
2711 - current,
2712 - workInProgress,
2713 - renderLanes,
2730 + const capturedValue = createCapturedValue(
2731 new Error(
2732 'There was an error while hydrating this Suspense boundary. ' +
2733 'Switched to client rendering.',
2734 ),
2735 );
2736 + return retrySuspenseComponentWithoutHydrating(
2737 + current,
2738 + workInProgress,
2739 + renderLanes,
2740 + capturedValue,
2741 + );
2742 } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) {
2743 // Something suspended and we should still be in dehydrated mode.
2744 // Leave the existing child in place.
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+48 -25
@@ -237,7 +237,11 @@ import {
237 import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old';
238 import {setWorkInProgressVersion} from './ReactMutableSource.old';
239 import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.old';
240 -import {createCapturedValue} from './ReactCapturedValue';
240 +import {
241 + createCapturedValue,
242 + createCapturedValueAtFiber,
243 + type CapturedValue,
244 +} from './ReactCapturedValue';
245 import {createClassErrorUpdate} from './ReactFiberThrow.old';
246 import is from 'shared/objectIs';
247 import {
@@ -1074,7 +1078,7 @@ function updateClassComponent(
1078 // Schedule the error boundary to re-render using updated state
1079 const update = createClassErrorUpdate(
1080 workInProgress,
1077 - createCapturedValue(error, workInProgress),
1081 + createCapturedValueAtFiber(error, workInProgress),
1082 lane,
1083 );
1084 enqueueCapturedUpdate(workInProgress, update);
@@ -1322,10 +1326,13 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1326 if (workInProgress.flags & ForceClientRender) {
1327 // Something errored during a previous attempt to hydrate the shell, so we
1328 // forced a client render.
1325 - const recoverableError = new Error(
1326 - 'There was an error while hydrating. Because the error happened outside ' +
1327 - 'of a Suspense boundary, the entire root will switch to ' +
1328 - 'client rendering.',
1329 + const recoverableError = createCapturedValueAtFiber(
1330 + new Error(
1331 + 'There was an error while hydrating. Because the error happened outside ' +
1332 + 'of a Suspense boundary, the entire root will switch to ' +
1333 + 'client rendering.',
1334 + ),
1335 + workInProgress,
1336 );
1337 return mountHostRootWithoutHydrating(
1338 current,
@@ -1335,9 +1342,12 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1342 recoverableError,
1343 );
1344 } else if (nextChildren !== prevChildren) {
1338 - const recoverableError = new Error(
1339 - 'This root received an early update, before anything was able ' +
1340 - 'hydrate. Switched the entire root to client rendering.',
1345 + const recoverableError = createCapturedValueAtFiber(
1346 + new Error(
1347 + 'This root received an early update, before anything was able ' +
1348 + 'hydrate. Switched the entire root to client rendering.',
1349 + ),
1350 + workInProgress,
1351 );
1352 return mountHostRootWithoutHydrating(
1353 current,
@@ -1400,7 +1410,7 @@ function mountHostRootWithoutHydrating(
1410 workInProgress: Fiber,
1411 nextChildren: ReactNodeList,
1412 renderLanes: Lanes,
1403 - recoverableError: Error,
1413 + recoverableError: CapturedValue<mixed>,
1414 ) {
1415 // Revert to client rendering.
1416 resetHydrationState();
@@ -2429,7 +2439,7 @@ function retrySuspenseComponentWithoutHydrating(
2439 current: Fiber,
2440 workInProgress: Fiber,
2441 renderLanes: Lanes,
2432 - recoverableError: Error | null,
2442 + recoverableError: CapturedValue<mixed> | null,
2443 ) {
2444 // Falling back to client rendering. Because this has performance
2445 // implications, it's considered a recoverable error, even though the user
@@ -2574,22 +2584,29 @@ function updateDehydratedSuspenseComponent(
2584 // This boundary is in a permanent fallback state. In this case, we'll never
2585 // get an update and we'll never be able to hydrate the final content. Let's just try the
2586 // client side render instead.
2577 - const {errorMessage} = getSuspenseInstanceFallbackErrorDetails(
2578 - suspenseInstance,
2579 - );
2580 - const error = errorMessage
2587 + let digest, message, stack;
2588 + if (__DEV__) {
2589 + ({digest, message, stack} = getSuspenseInstanceFallbackErrorDetails(
2590 + suspenseInstance,
2591 + ));
2592 + } else {
2593 + ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
2594 + }
2595 +
2596 + const error = message
2597 ? // eslint-disable-next-line react-internal/prod-error-codes
2582 - new Error(errorMessage)
2598 + new Error(message)
2599 : new Error(
2600 'The server could not finish this Suspense boundary, likely ' +
2601 'due to an error during server rendering. Switched to ' +
2602 'client rendering.',
2603 );
2604 + const capturedValue = createCapturedValue(error, digest, stack);
2605 return retrySuspenseComponentWithoutHydrating(
2606 current,
2607 workInProgress,
2608 renderLanes,
2592 - error,
2609 + capturedValue,
2610 );
2611 }
2612
@@ -2650,10 +2667,7 @@ function updateDehydratedSuspenseComponent(
2667 // skip hydration.
2668 // Delay having to do this as long as the suspense timeout allows us.
2669 renderDidSuspendDelayIfPossible();
2653 - return retrySuspenseComponentWithoutHydrating(
2654 - current,
2655 - workInProgress,
2656 - renderLanes,
2670 + const capturedValue = createCapturedValue(
2671 new Error(
2672 'This Suspense boundary received an update before it finished ' +
2673 'hydrating. This caused the boundary to switch to client rendering. ' +
@@ -2661,6 +2675,12 @@ function updateDehydratedSuspenseComponent(
2675 'in startTransition.',
2676 ),
2677 );
2678 + return retrySuspenseComponentWithoutHydrating(
2679 + current,
2680 + workInProgress,
2681 + renderLanes,
2682 + capturedValue,
2683 + );
2684 } else if (isSuspenseInstancePending(suspenseInstance)) {
2685 // This component is still pending more data from the server, so we can't hydrate its
2686 // content. We treat it as if this component suspended itself. It might seem as if
@@ -2707,15 +2727,18 @@ function updateDehydratedSuspenseComponent(
2727 if (workInProgress.flags & ForceClientRender) {
2728 // Something errored during hydration. Try again without hydrating.
2729 workInProgress.flags &= ~ForceClientRender;
2710 - return retrySuspenseComponentWithoutHydrating(
2711 - current,
2712 - workInProgress,
2713 - renderLanes,
2730 + const capturedValue = createCapturedValue(
2731 new Error(
2732 'There was an error while hydrating this Suspense boundary. ' +
2733 'Switched to client rendering.',
2734 ),
2735 );
2736 + return retrySuspenseComponentWithoutHydrating(
2737 + current,
2738 + workInProgress,
2739 + renderLanes,
2740 + capturedValue,
2741 + );
2742 } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) {
2743 // Something suspended and we should still be in dehydrated mode.
2744 // Leave the existing child in place.
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+3 -2
@@ -19,6 +19,7 @@ import type {
19 } from './ReactFiberHostConfig';
20 import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
21 import type {TreeContext} from './ReactFiberTreeContext.new';
22 +import type {CapturedValue} from './ReactCapturedValue';
23
24 import {
25 HostComponent,
@@ -86,7 +87,7 @@ let isHydrating: boolean = false;
87 let didSuspendOrErrorDEV: boolean = false;
88
89 // Hydration errors that were thrown inside this boundary
89 -let hydrationErrors: Array<mixed> | null = null;
90 +let hydrationErrors: Array<CapturedValue<mixed>> | null = null;
91
92 function warnIfHydrating() {
93 if (__DEV__) {
@@ -680,7 +681,7 @@ function getIsHydrating(): boolean {
681 return isHydrating;
682 }
683
683 -export function queueHydrationError(error: mixed): void {
684 +export function queueHydrationError(error: CapturedValue<mixed>): void {
685 if (hydrationErrors === null) {
686 hydrationErrors = [error];
687 } else {
packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+3 -2
@@ -19,6 +19,7 @@ import type {
19 } from './ReactFiberHostConfig';
20 import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
21 import type {TreeContext} from './ReactFiberTreeContext.old';
22 +import type {CapturedValue} from './ReactCapturedValue';
23
24 import {
25 HostComponent,
@@ -86,7 +87,7 @@ let isHydrating: boolean = false;
87 let didSuspendOrErrorDEV: boolean = false;
88
89 // Hydration errors that were thrown inside this boundary
89 -let hydrationErrors: Array<mixed> | null = null;
90 +let hydrationErrors: Array<CapturedValue<mixed>> | null = null;
91
92 function warnIfHydrating() {
93 if (__DEV__) {
@@ -680,7 +681,7 @@ function getIsHydrating(): boolean {
681 return isHydrating;
682 }
683
683 -export function queueHydrationError(error: mixed): void {
684 +export function queueHydrationError(error: CapturedValue<mixed>): void {
685 if (hydrationErrors === null) {
686 hydrationErrors = [error];
687 } else {
packages/react-reconciler/src/ReactFiberThrow.new.js
+5 -5
@@ -41,7 +41,7 @@ import {
41 enableLazyContextPropagation,
42 enableUpdaterTracking,
43 } from 'shared/ReactFeatureFlags';
44 -import {createCapturedValue} from './ReactCapturedValue';
44 +import {createCapturedValueAtFiber} from './ReactCapturedValue';
45 import {
46 enqueueCapturedUpdate,
47 createUpdate,
@@ -517,7 +517,7 @@ function throwException(
517
518 // Even though the user may not be affected by this error, we should
519 // still log it so it can be fixed.
520 - queueHydrationError(value);
520 + queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
521 return;
522 }
523 } else {
@@ -525,12 +525,12 @@ function throwException(
525 }
526 }
527
528 + value = createCapturedValueAtFiber(value, sourceFiber);
529 + renderDidError(value);
530 +
531 // We didn't find a boundary that could handle this type of exception. Start
532 // over and traverse parent path again, this time treating the exception
533 // as an error.
531 - renderDidError(value);
532 -
533 - value = createCapturedValue(value, sourceFiber);
534 let workInProgress = returnFiber;
535 do {
536 switch (workInProgress.tag) {
packages/react-reconciler/src/ReactFiberThrow.old.js
+5 -5
@@ -41,7 +41,7 @@ import {
41 enableLazyContextPropagation,
42 enableUpdaterTracking,
43 } from 'shared/ReactFeatureFlags';
44 -import {createCapturedValue} from './ReactCapturedValue';
44 +import {createCapturedValueAtFiber} from './ReactCapturedValue';
45 import {
46 enqueueCapturedUpdate,
47 createUpdate,
@@ -517,7 +517,7 @@ function throwException(
517
518 // Even though the user may not be affected by this error, we should
519 // still log it so it can be fixed.
520 - queueHydrationError(value);
520 + queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
521 return;
522 }
523 } else {
@@ -525,12 +525,12 @@ function throwException(
525 }
526 }
527
528 + value = createCapturedValueAtFiber(value, sourceFiber);
529 + renderDidError(value);
530 +
531 // We didn't find a boundary that could handle this type of exception. Start
532 // over and traverse parent path again, this time treating the exception
533 // as an error.
531 - renderDidError(value);
532 -
533 - value = createCapturedValue(value, sourceFiber);
534 let workInProgress = returnFiber;
535 do {
536 switch (workInProgress.tag) {
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+19 -10
@@ -187,7 +187,10 @@ import {
187 ContextOnlyDispatcher,
188 getIsUpdatingOpaqueValueInRenderPhaseInDEV,
189 } from './ReactFiberHooks.new';
190 -import {createCapturedValue} from './ReactCapturedValue';
190 +import {
191 + createCapturedValueAtFiber,
192 + type CapturedValue,
193 +} from './ReactCapturedValue';
194 import {
195 push as pushToStack,
196 pop as popFromStack,
@@ -310,10 +313,14 @@ let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
313 // Lanes that were pinged (in an interleaved event) during this render.
314 let workInProgressRootPingedLanes: Lanes = NoLanes;
315 // Errors that are thrown during the render phase.
313 -let workInProgressRootConcurrentErrors: Array<mixed> | null = null;
316 +let workInProgressRootConcurrentErrors: Array<
317 + CapturedValue<mixed>,
318 +> | null = null;
319 // These are errors that we recovered from without surfacing them to the UI.
320 // We will log them once the tree commits.
316 -let workInProgressRootRecoverableErrors: Array<mixed> | null = null;
321 +let workInProgressRootRecoverableErrors: Array<
322 + CapturedValue<mixed>,
323 +> | null = null;
324
325 // The most recent time we committed a fallback. This lets us ensure a train
326 // model where we don't commit new loading states in too quick succession.
@@ -998,7 +1005,7 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
1005 return exitStatus;
1006 }
1007
1001 -export function queueRecoverableErrors(errors: Array<mixed>) {
1008 +export function queueRecoverableErrors(errors: Array<CapturedValue<mixed>>) {
1009 if (workInProgressRootRecoverableErrors === null) {
1010 workInProgressRootRecoverableErrors = errors;
1011 } else {
@@ -1629,7 +1636,7 @@ export function renderDidSuspendDelayIfPossible(): void {
1636 }
1637 }
1638
1632 -export function renderDidError(error: mixed) {
1639 +export function renderDidError(error: CapturedValue<mixed>) {
1640 if (workInProgressRootExitStatus !== RootSuspendedWithDelay) {
1641 workInProgressRootExitStatus = RootErrored;
1642 }
@@ -1950,7 +1957,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
1957
1958 function commitRoot(
1959 root: FiberRoot,
1953 - recoverableErrors: null | Array<mixed>,
1960 + recoverableErrors: null | Array<CapturedValue<mixed>>,
1961 transitions: Array<Transition> | null,
1962 ) {
1963 // TODO: This no longer makes any sense. We already wrap the mutation and
@@ -1977,7 +1984,7 @@ function commitRoot(
1984
1985 function commitRootImpl(
1986 root: FiberRoot,
1980 - recoverableErrors: null | Array<mixed>,
1987 + recoverableErrors: null | Array<CapturedValue<mixed>>,
1988 transitions: Array<Transition> | null,
1989 renderPriorityLevel: EventPriority,
1990 ) {
@@ -2274,7 +2281,9 @@ function commitRootImpl(
2281 const onRecoverableError = root.onRecoverableError;
2282 for (let i = 0; i < recoverableErrors.length; i++) {
2283 const recoverableError = recoverableErrors[i];
2277 - onRecoverableError(recoverableError);
2284 + const componentStack = recoverableError.stack;
2285 + const digest = recoverableError.digest;
2286 + onRecoverableError(recoverableError.value, {componentStack, digest});
2287 }
2288 }
2289
@@ -2554,7 +2563,7 @@ function captureCommitPhaseErrorOnRoot(
2563 sourceFiber: Fiber,
2564 error: mixed,
2565 ) {
2557 - const errorInfo = createCapturedValue(error, sourceFiber);
2566 + const errorInfo = createCapturedValueAtFiber(error, sourceFiber);
2567 const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
2568 const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane));
2569 const eventTime = requestEventTime();
@@ -2599,7 +2608,7 @@ export function captureCommitPhaseError(
2608 (typeof instance.componentDidCatch === 'function' &&
2609 !isAlreadyFailedLegacyErrorBoundary(instance))
2610 ) {
2602 - const errorInfo = createCapturedValue(error, sourceFiber);
2611 + const errorInfo = createCapturedValueAtFiber(error, sourceFiber);
2612 const update = createClassErrorUpdate(
2613 fiber,
2614 errorInfo,
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+19 -10
@@ -187,7 +187,10 @@ import {
187 ContextOnlyDispatcher,
188 getIsUpdatingOpaqueValueInRenderPhaseInDEV,
189 } from './ReactFiberHooks.old';
190 -import {createCapturedValue} from './ReactCapturedValue';
190 +import {
191 + createCapturedValueAtFiber,
192 + type CapturedValue,
193 +} from './ReactCapturedValue';
194 import {
195 push as pushToStack,
196 pop as popFromStack,
@@ -310,10 +313,14 @@ let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
313 // Lanes that were pinged (in an interleaved event) during this render.
314 let workInProgressRootPingedLanes: Lanes = NoLanes;
315 // Errors that are thrown during the render phase.
313 -let workInProgressRootConcurrentErrors: Array<mixed> | null = null;
316 +let workInProgressRootConcurrentErrors: Array<
317 + CapturedValue<mixed>,
318 +> | null = null;
319 // These are errors that we recovered from without surfacing them to the UI.
320 // We will log them once the tree commits.
316 -let workInProgressRootRecoverableErrors: Array<mixed> | null = null;
321 +let workInProgressRootRecoverableErrors: Array<
322 + CapturedValue<mixed>,
323 +> | null = null;
324
325 // The most recent time we committed a fallback. This lets us ensure a train
326 // model where we don't commit new loading states in too quick succession.
@@ -998,7 +1005,7 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
1005 return exitStatus;
1006 }
1007
1001 -export function queueRecoverableErrors(errors: Array<mixed>) {
1008 +export function queueRecoverableErrors(errors: Array<CapturedValue<mixed>>) {
1009 if (workInProgressRootRecoverableErrors === null) {
1010 workInProgressRootRecoverableErrors = errors;
1011 } else {
@@ -1629,7 +1636,7 @@ export function renderDidSuspendDelayIfPossible(): void {
1636 }
1637 }
1638
1632 -export function renderDidError(error: mixed) {
1639 +export function renderDidError(error: CapturedValue<mixed>) {
1640 if (workInProgressRootExitStatus !== RootSuspendedWithDelay) {
1641 workInProgressRootExitStatus = RootErrored;
1642 }
@@ -1950,7 +1957,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
1957
1958 function commitRoot(
1959 root: FiberRoot,
1953 - recoverableErrors: null | Array<mixed>,
1960 + recoverableErrors: null | Array<CapturedValue<mixed>>,
1961 transitions: Array<Transition> | null,
1962 ) {
1963 // TODO: This no longer makes any sense. We already wrap the mutation and
@@ -1977,7 +1984,7 @@ function commitRoot(
1984
1985 function commitRootImpl(
1986 root: FiberRoot,
1980 - recoverableErrors: null | Array<mixed>,
1987 + recoverableErrors: null | Array<CapturedValue<mixed>>,
1988 transitions: Array<Transition> | null,
1989 renderPriorityLevel: EventPriority,
1990 ) {
@@ -2268,7 +2275,9 @@ function commitRootImpl(
2275 const onRecoverableError = root.onRecoverableError;
2276 for (let i = 0; i < recoverableErrors.length; i++) {
2277 const recoverableError = recoverableErrors[i];
2271 - onRecoverableError(recoverableError);
2278 + const componentStack = recoverableError.stack;
2279 + const digest = recoverableError.digest;
2280 + onRecoverableError(recoverableError.value, {componentStack, digest});
2281 }
2282 }
2283
@@ -2548,7 +2557,7 @@ function captureCommitPhaseErrorOnRoot(
2557 sourceFiber: Fiber,
2558 error: mixed,
2559 ) {
2551 - const errorInfo = createCapturedValue(error, sourceFiber);
2560 + const errorInfo = createCapturedValueAtFiber(error, sourceFiber);
2561 const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
2562 const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane));
2563 const eventTime = requestEventTime();
@@ -2593,7 +2602,7 @@ export function captureCommitPhaseError(
2602 (typeof instance.componentDidCatch === 'function' &&
2603 !isAlreadyFailedLegacyErrorBoundary(instance))
2604 ) {
2596 - const errorInfo = createCapturedValue(error, sourceFiber);
2605 + const errorInfo = createCapturedValueAtFiber(error, sourceFiber);
2606 const update = createClassErrorUpdate(
2607 fiber,
2608 errorInfo,
packages/react-reconciler/src/ReactInternalTypes.js
+4 -1
@@ -247,7 +247,10 @@ type BaseFiberRootProperties = {|
247 // a reference to.
248 identifierPrefix: string,
249
250 - onRecoverableError: (error: mixed) => void,
250 + onRecoverableError: (
251 + error: mixed,
252 + errorInfo: {digest?: ?string, componentStack?: ?string},
253 + ) => void,
254 |};
255
256 // The following attributes are only used by DevTools and are only present in DEV builds.
packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js
+1 -1
@@ -192,7 +192,7 @@ describe('ReactDOMServerFB', () => {
192 expect(remaining).toEqual('');
193
194 expect(errors).toEqual([
195 - 'This Suspense boundary was aborted by the server',
195 + 'This Suspense boundary was aborted by the server.',
196 ]);
197 });
198 });
packages/react-server/src/ReactFizzServer.js
+13 -13
@@ -131,7 +131,7 @@ type LegacyContext = {
131 type SuspenseBoundary = {
132 id: SuspenseBoundaryID,
133 rootSegmentID: number,
134 - errorHash: ?string, // the error hash if it errors
134 + errorDigest: ?string, // the error hash if it errors
135 errorMessage?: string, // the error string if it errors
136 errorComponentStack?: string, // the error component stack if it errors
137 forceClientRender: boolean, // if it errors or infinitely suspends
@@ -323,7 +323,7 @@ function createSuspenseBoundary(
323 completedSegments: [],
324 byteSize: 0,
325 fallbackAbortableTasks,
326 - errorHash: null,
326 + errorDigest: null,
327 };
328 }
329
@@ -463,14 +463,14 @@ function captureBoundaryErrorDetailsDev(
463 function logRecoverableError(request: Request, error: any): ?string {
464 // If this callback errors, we intentionally let that error bubble up to become a fatal error
465 // so that someone fixes the error reporting instead of hiding it.
466 - const errorHash = request.onError(error);
467 - if (errorHash != null && typeof errorHash !== 'string') {
466 + const errorDigest = request.onError(error);
467 + if (errorDigest != null && typeof errorDigest !== 'string') {
468 // eslint-disable-next-line react-internal/prod-error-codes
469 throw new Error(
470 - `onError returned something with a type other than "string". onError should return a string and may return null or undefined but must not return anything else. It received something of type "${typeof errorHash}" instead`,
470 + `onError returned something with a type other than "string". onError should return a string and may return null or undefined but must not return anything else. It received something of type "${typeof errorDigest}" instead`,
471 );
472 }
473 - return errorHash;
473 + return errorDigest;
474 }
475
476 function fatalError(request: Request, error: mixed): void {
@@ -568,7 +568,7 @@ function renderSuspenseBoundary(
568 } catch (error) {
569 contentRootSegment.status = ERRORED;
570 newBoundary.forceClientRender = true;
571 - newBoundary.errorHash = logRecoverableError(request, error);
571 + newBoundary.errorDigest = logRecoverableError(request, error);
572 if (__DEV__) {
573 captureBoundaryErrorDetailsDev(newBoundary, error);
574 }
@@ -1488,14 +1488,14 @@ function erroredTask(
1488 error: mixed,
1489 ) {
1490 // Report the error to a global handler.
1491 - const errorHash = logRecoverableError(request, error);
1491 + const errorDigest = logRecoverableError(request, error);
1492 if (boundary === null) {
1493 fatalError(request, error);
1494 } else {
1495 boundary.pendingTasks--;
1496 if (!boundary.forceClientRender) {
1497 boundary.forceClientRender = true;
1498 - boundary.errorHash = errorHash;
1498 + boundary.errorDigest = errorDigest;
1499 if (__DEV__) {
1500 captureBoundaryErrorDetailsDev(boundary, error);
1501 }
@@ -1554,9 +1554,9 @@ function abortTask(task: Task): void {
1554 if (!boundary.forceClientRender) {
1555 boundary.forceClientRender = true;
1556 const error = new Error(
1557 - 'This Suspense boundary was aborted by the server',
1557 + 'This Suspense boundary was aborted by the server.',
1558 );
1559 - boundary.errorHash = request.onError(error);
1559 + boundary.errorDigest = request.onError(error);
1560 if (__DEV__) {
1561 captureBoundaryErrorDetailsDev(boundary, error);
1562 }
@@ -1838,7 +1838,7 @@ function flushSegment(
1838 writeStartClientRenderedSuspenseBoundary(
1839 destination,
1840 request.responseState,
1841 - boundary.errorHash,
1841 + boundary.errorDigest,
1842 boundary.errorMessage,
1843 boundary.errorComponentStack,
1844 );
@@ -1921,7 +1921,7 @@ function flushClientRenderedBoundary(
1921 destination,
1922 request.responseState,
1923 boundary.id,
1924 - boundary.errorHash,
1924 + boundary.errorDigest,
1925 boundary.errorMessage,
1926 boundary.errorComponentStack,
1927 );
scripts/error-codes/codes.json
+1 -1
@@ -417,6 +417,6 @@
417 "429": "ServerContext: %s already defined",
418 "430": "ServerContext can only have a value prop and children. Found: %s",
419 "431": "React elements are not allowed in ServerContext",
420 - "432": "This Suspense boundary was aborted by the server",
420 + "432": "This Suspense boundary was aborted by the server.",
421 "433": "useId can only be used while React is rendering"
422 }