@samitouri / QOS-React-1 / commits / b9149cc6e6

Include regular stack trace in serialized errors from Fizz (#28684)

We previously only included the component stack. Cleaned up the fields in Fizz server that wasn't using consistent hidden classes in dev vs prod. Added a prefix to errors serialized from server rendering. It can be a bit confusing to see where this error came from otherwise since it didn't come from elsewhere on the client. It's really kind of confusing with other recoverable errors that happen on the client too.

Sebastian Markbåge committed Mar 30, 2024 at 11:08 UTC b9149cc6e6442389accf1f7c34a77ba2e6e52b5e
13 files changed +229 -104
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+9 -2
@@ -1226,15 +1226,21 @@ export function isSuspenseInstanceFallback(
1226
1227 export function getSuspenseInstanceFallbackErrorDetails(
1228 instance: SuspenseInstance,
1229 -): {digest: ?string, message?: string, stack?: string} {
1229 +): {
1230 + digest: ?string,
1231 + message?: string,
1232 + stack?: string,
1233 + componentStack?: string,
1234 +} {
1235 const dataset =
1236 instance.nextSibling && ((instance.nextSibling: any): HTMLElement).dataset;
1232 - let digest, message, stack;
1237 + let digest, message, stack, componentStack;
1238 if (dataset) {
1239 digest = dataset.dgst;
1240 if (__DEV__) {
1241 message = dataset.msg;
1242 stack = dataset.stck;
1243 + componentStack = dataset.cstck;
1244 }
1245 }
1246 if (__DEV__) {
@@ -1242,6 +1248,7 @@ export function getSuspenseInstanceFallbackErrorDetails(
1248 message,
1249 digest,
1250 stack,
1251 + componentStack,
1252 };
1253 } else {
1254 // Object gets DCE'd if constructed in tail position and matches callsite destructuring
packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js
+1
@@ -94,6 +94,7 @@ function handleNode(node_: Node) {
94 dataset['dgst'],
95 dataset['msg'],
96 dataset['stck'],
97 + dataset['cstck'],
98 );
99 node.remove();
100 } else if (dataset['rri'] != null) {
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+40 -10
@@ -3801,6 +3801,8 @@ const clientRenderedSuspenseBoundaryError1B =
3801 stringToPrecomputedChunk(' data-msg="');
3802 const clientRenderedSuspenseBoundaryError1C =
3803 stringToPrecomputedChunk(' data-stck="');
3804 +const clientRenderedSuspenseBoundaryError1D =
3805 + stringToPrecomputedChunk(' data-cstck="');
3806 const clientRenderedSuspenseBoundaryError2 =
3807 stringToPrecomputedChunk('></template>');
3808
@@ -3843,7 +3845,8 @@ export function writeStartClientRenderedSuspenseBoundary(
3845 destination: Destination,
3846 renderState: RenderState,
3847 errorDigest: ?string,
3846 - errorMesssage: ?string,
3848 + errorMessage: ?string,
3849 + errorStack: ?string,
3850 errorComponentStack: ?string,
3851 ): boolean {
3852 let result;
@@ -3861,19 +3864,27 @@ export function writeStartClientRenderedSuspenseBoundary(
3864 );
3865 }
3866 if (__DEV__) {
3864 - if (errorMesssage) {
3867 + if (errorMessage) {
3868 writeChunk(destination, clientRenderedSuspenseBoundaryError1B);
3869 writeChunk(
3870 destination,
3868 - stringToChunk(escapeTextForBrowser(errorMesssage)),
3871 + stringToChunk(escapeTextForBrowser(errorMessage)),
3872 );
3873 writeChunk(
3874 destination,
3875 clientRenderedSuspenseBoundaryErrorAttrInterstitial,
3876 );
3877 }
3875 - if (errorComponentStack) {
3878 + if (errorStack) {
3879 writeChunk(destination, clientRenderedSuspenseBoundaryError1C);
3880 + writeChunk(destination, stringToChunk(escapeTextForBrowser(errorStack)));
3881 + writeChunk(
3882 + destination,
3883 + clientRenderedSuspenseBoundaryErrorAttrInterstitial,
3884 + );
3885 + }
3886 + if (errorComponentStack) {
3887 + writeChunk(destination, clientRenderedSuspenseBoundaryError1D);
3888 writeChunk(
3889 destination,
3890 stringToChunk(escapeTextForBrowser(errorComponentStack)),
@@ -4236,6 +4247,7 @@ const clientRenderData1 = stringToPrecomputedChunk(
4247 const clientRenderData2 = stringToPrecomputedChunk('" data-dgst="');
4248 const clientRenderData3 = stringToPrecomputedChunk('" data-msg="');
4249 const clientRenderData4 = stringToPrecomputedChunk('" data-stck="');
4250 +const clientRenderData5 = stringToPrecomputedChunk('" data-cstck="');
4251 const clientRenderDataEnd = dataElementQuotedEnd;
4252
4253 export function writeClientRenderBoundaryInstruction(
@@ -4244,8 +4256,9 @@ export function writeClientRenderBoundaryInstruction(
4256 renderState: RenderState,
4257 id: number,
4258 errorDigest: ?string,
4247 - errorMessage?: string,
4248 - errorComponentStack?: string,
4259 + errorMessage: ?string,
4260 + errorStack: ?string,
4261 + errorComponentStack: ?string,
4262 ): boolean {
4263 const scriptFormat =
4264 !enableFizzExternalRuntime ||
@@ -4276,7 +4289,7 @@ export function writeClientRenderBoundaryInstruction(
4289 writeChunk(destination, clientRenderScript1A);
4290 }
4291
4279 - if (errorDigest || errorMessage || errorComponentStack) {
4292 + if (errorDigest || errorMessage || errorStack || errorComponentStack) {
4293 if (scriptFormat) {
4294 // ,"JSONString"
4295 writeChunk(destination, clientRenderErrorScriptArgInterstitial);
@@ -4293,7 +4306,7 @@ export function writeClientRenderBoundaryInstruction(
4306 );
4307 }
4308 }
4296 - if (errorMessage || errorComponentStack) {
4309 + if (errorMessage || errorStack || errorComponentStack) {
4310 if (scriptFormat) {
4311 // ,"JSONString"
4312 writeChunk(destination, clientRenderErrorScriptArgInterstitial);
@@ -4310,6 +4323,23 @@ export function writeClientRenderBoundaryInstruction(
4323 );
4324 }
4325 }
4326 + if (errorStack || errorComponentStack) {
4327 + // ,"JSONString"
4328 + if (scriptFormat) {
4329 + writeChunk(destination, clientRenderErrorScriptArgInterstitial);
4330 + writeChunk(
4331 + destination,
4332 + stringToChunk(escapeJSStringsForInstructionScripts(errorStack || '')),
4333 + );
4334 + } else {
4335 + // " data-stck="HTMLString
4336 + writeChunk(destination, clientRenderData4);
4337 + writeChunk(
4338 + destination,
4339 + stringToChunk(escapeTextForBrowser(errorStack || '')),
4340 + );
4341 + }
4342 + }
4343 if (errorComponentStack) {
4344 // ,"JSONString"
4345 if (scriptFormat) {
@@ -4321,8 +4351,8 @@ export function writeClientRenderBoundaryInstruction(
4351 ),
4352 );
4353 } else {
4324 - // " data-stck="HTMLString
4325 - writeChunk(destination, clientRenderData4);
4354 + // " data-cstck="HTMLString
4355 + writeChunk(destination, clientRenderData5);
4356 writeChunk(
4357 destination,
4358 stringToChunk(escapeTextForBrowser(errorComponentStack)),
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+2
@@ -219,6 +219,7 @@ export function writeStartClientRenderedSuspenseBoundary(
219 // flushing these error arguments are not currently supported in this legacy streaming format.
220 errorDigest: ?string,
221 errorMessage: ?string,
222 + errorStack: ?string,
223 errorComponentStack: ?string,
224 ): boolean {
225 if (renderState.generateStaticMarkup) {
@@ -231,6 +232,7 @@ export function writeStartClientRenderedSuspenseBoundary(
232 renderState,
233 errorDigest,
234 errorMessage,
235 + errorStack,
236 errorComponentStack,
237 );
238 }
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1 -1
@@ -2,7 +2,7 @@
2 // The build script is at scripts/rollup/generate-inline-fizz-runtime.js.
3 // Run `yarn generate-inline-fizz-runtime` to generate.
4 export const clientRenderBoundary =
5 - '$RX=function(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())};';
5 + '$RX=function(b,c,d,e,f){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),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
6 export const completeBoundary =
7 '$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
8 export const completeBoundaryWithStyles =
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
+3 -1
@@ -19,6 +19,7 @@ export function clientRenderBoundary(
19 suspenseBoundaryID,
20 errorDigest,
21 errorMsg,
22 + errorStack,
23 errorComponentStack,
24 ) {
25 // Find the fallback's first element.
@@ -36,7 +37,8 @@ export function clientRenderBoundary(
37 const dataset = suspenseIdNode.dataset;
38 if (errorDigest) dataset['dgst'] = errorDigest;
39 if (errorMsg) dataset['msg'] = errorMsg;
39 - if (errorComponentStack) dataset['stck'] = errorComponentStack;
40 + if (errorStack) dataset['stck'] = errorStack;
41 + if (errorComponentStack) dataset['cstck'] = errorComponentStack;
42 // Tell React to retry it if the parent already hydrated.
43 if (suspenseNode['_reactRetry']) {
44 suspenseNode['_reactRetry']();
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+44 -18
@@ -796,7 +796,8 @@ describe('ReactDOMFizzServer', () => {
796 errors,
797 [
798 [
799 - theError.message,
799 + 'Switched to client rendering because the server rendering errored:\n\n' +
800 + theError.message,
801 expectedDigest,
802 componentStack(['Lazy', 'Suspense', 'div', 'App']),
803 ],
@@ -919,7 +920,8 @@ describe('ReactDOMFizzServer', () => {
920 errors,
921 [
922 [
922 - theError.message,
923 + 'Switched to client rendering because the server rendering errored:\n\n' +
924 + theError.message,
925 expectedDigest,
926 componentStack(['Lazy', 'Suspense', 'div', 'App']),
927 ],
@@ -1002,7 +1004,8 @@ describe('ReactDOMFizzServer', () => {
1004 errors,
1005 [
1006 [
1005 - theError.message,
1007 + 'Switched to client rendering because the server rendering errored:\n\n' +
1008 + theError.message,
1009 expectedDigest,
1010 componentStack([
1011 'Erroring',
@@ -1088,7 +1091,8 @@ describe('ReactDOMFizzServer', () => {
1091 errors,
1092 [
1093 [
1091 - theError.message,
1094 + 'Switched to client rendering because the server rendering errored:\n\n' +
1095 + theError.message,
1096 expectedDigest,
1097 componentStack(['Lazy', 'Suspense', 'div', 'App']),
1098 ],
@@ -1414,13 +1418,15 @@ describe('ReactDOMFizzServer', () => {
1418 errors,
1419 [
1420 [
1417 - 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.',
1421 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
1422 + 'The render was aborted by the server without a reason.',
1423 expectedDigest,
1424 // We get the stack of the task when it was aborted which is why we see `h1`
1425 componentStack(['h1', 'Suspense', 'div', 'App']),
1426 ],
1427 [
1423 - 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.',
1428 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
1429 + 'The render was aborted by the server without a reason.',
1430 expectedDigest,
1431 componentStack(['Suspense', 'main', 'div', 'App']),
1432 ],
@@ -2155,7 +2161,8 @@ describe('ReactDOMFizzServer', () => {
2161 errors,
2162 [
2163 [
2158 - theError.message,
2164 + 'Switched to client rendering because the server rendering errored:\n\n' +
2165 + theError.message,
2166 expectedDigest,
2167 componentStack([
2168 'AsyncText',
@@ -3441,12 +3448,14 @@ describe('ReactDOMFizzServer', () => {
3448 errors,
3449 [
3450 [
3444 - 'The server did not finish this Suspense boundary: foobar',
3451 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
3452 + 'foobar',
3453 'a digest',
3454 componentStack(['Suspense', 'p', 'div', 'App']),
3455 ],
3456 [
3449 - 'The server did not finish this Suspense boundary: foobar',
3457 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
3458 + 'foobar',
3459 'a digest',
3460 componentStack(['Suspense', 'span', 'div', 'App']),
3461 ],
@@ -3522,12 +3531,14 @@ describe('ReactDOMFizzServer', () => {
3531 errors,
3532 [
3533 [
3525 - 'The server did not finish this Suspense boundary: uh oh',
3534 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
3535 + 'uh oh',
3536 'a digest',
3537 componentStack(['Suspense', 'p', 'div', 'App']),
3538 ],
3539 [
3530 - 'The server did not finish this Suspense boundary: uh oh',
3540 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
3541 + 'uh oh',
3542 'a digest',
3543 componentStack(['Suspense', 'span', 'div', 'App']),
3544 ],
@@ -4001,7 +4012,8 @@ describe('ReactDOMFizzServer', () => {
4012 errors,
4013 [
4014 [
4004 - theError.message,
4015 + 'Switched to client rendering because the server rendering errored:\n\n' +
4016 + theError.message,
4017 expectedDigest,
4018 componentStack(['Erroring', 'Suspense', 'div', 'App']),
4019 ],
@@ -6782,7 +6794,14 @@ describe('ReactDOMFizzServer', () => {
6794
6795 expect(recoverableErrors).toEqual(
6796 __DEV__
6785 - ? ['server error', 'replay error', 'server error']
6797 + ? [
6798 + 'Switched to client rendering because the server rendering errored:\n\n' +
6799 + 'server error',
6800 + 'Switched to client rendering because the server rendering errored:\n\n' +
6801 + 'replay error',
6802 + 'Switched to client rendering because the server rendering errored:\n\n' +
6803 + 'server error',
6804 + ]
6805 : [
6806 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
6807 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
@@ -6941,8 +6960,10 @@ describe('ReactDOMFizzServer', () => {
6960 expect(recoverableErrors).toEqual(
6961 __DEV__
6962 ? [
6944 - 'The server did not finish this Suspense boundary: aborted',
6945 - 'The server did not finish this Suspense boundary: aborted',
6963 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
6964 + 'aborted',
6965 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
6966 + 'aborted',
6967 ]
6968 : [
6969 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
@@ -7113,8 +7134,10 @@ describe('ReactDOMFizzServer', () => {
7134 // It surfaced in two different suspense boundaries.
7135 __DEV__
7136 ? [
7116 - 'The server did not finish this Suspense boundary: replay error',
7117 - 'The server did not finish this Suspense boundary: replay error',
7137 + 'Switched to client rendering because the server rendering errored:\n\n' +
7138 + 'replay error',
7139 + 'Switched to client rendering because the server rendering errored:\n\n' +
7140 + 'replay error',
7141 ]
7142 : [
7143 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
@@ -7240,7 +7263,10 @@ describe('ReactDOMFizzServer', () => {
7263
7264 expect(recoverableErrors).toEqual(
7265 __DEV__
7243 - ? ['server error']
7266 + ? [
7267 + 'Switched to client rendering because the server rendering errored:\n\n' +
7268 + 'server error',
7269 + ]
7270 : [
7271 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
7272 ],
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+12 -8
@@ -1292,10 +1292,12 @@ describe('ReactDOMServerHydration', () => {
1292 }
1293
1294 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1295 - [
1296 - "Caught [The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1297 - ]
1298 - `);
1295 + [
1296 + "Caught [Switched to client rendering because the server rendering aborted due to:
1297 +
1298 + The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1299 + ]
1300 + `);
1301 });
1302
1303 // @gate __DEV__
@@ -1318,10 +1320,12 @@ describe('ReactDOMServerHydration', () => {
1320 }
1321
1322 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1321 - [
1322 - "Caught [The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1323 - ]
1324 - `);
1323 + [
1324 + "Caught [Switched to client rendering because the server rendering aborted due to:
1325 +
1326 + The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1327 + ]
1328 + `);
1329 });
1330 });
1331
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+10 -5
@@ -2108,7 +2108,8 @@ describe('ReactDOMServerPartialHydration', () => {
2108 });
2109 if (__DEV__) {
2110 await waitForAll([
2111 - 'The server did not finish this Suspense boundary: The server used' +
2111 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2112 + 'The server used' +
2113 ' "renderToString" which does not support Suspense.',
2114 ]);
2115 } else {
@@ -2177,7 +2178,8 @@ describe('ReactDOMServerPartialHydration', () => {
2178 });
2179 if (__DEV__) {
2180 await waitForAll([
2180 - 'The server did not finish this Suspense boundary: The server used' +
2181 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2182 + 'The server used' +
2183 ' "renderToString" which does not support Suspense.',
2184 ]);
2185 } else {
@@ -2251,7 +2253,8 @@ describe('ReactDOMServerPartialHydration', () => {
2253 });
2254 if (__DEV__) {
2255 await waitForAll([
2254 - 'The server did not finish this Suspense boundary: The server used' +
2256 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2257 + 'The server used' +
2258 ' "renderToString" which does not support Suspense.',
2259 ]);
2260 } else {
@@ -2571,7 +2574,8 @@ describe('ReactDOMServerPartialHydration', () => {
2574 suspend = true;
2575 if (__DEV__) {
2576 await waitForAll([
2574 - 'The server did not finish this Suspense boundary: The server used' +
2577 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2578 + 'The server used' +
2579 ' "renderToString" which does not support Suspense.',
2580 ]);
2581 } else {
@@ -2641,7 +2645,8 @@ describe('ReactDOMServerPartialHydration', () => {
2645 });
2646 if (__DEV__) {
2647 await waitForAll([
2644 - 'The server did not finish this Suspense boundary: The server used' +
2648 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2649 + 'The server used' +
2650 ' "renderToString" which does not support Suspense.',
2651 ]);
2652 } else {
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+4 -2
@@ -674,7 +674,8 @@ describe('ReactDOMServerHydration', () => {
674 expect(errors.length).toBe(1);
675 if (__DEV__) {
676 expect(errors[0]).toBe(
677 - 'The server did not finish this Suspense boundary: The server used "renderToString" ' +
677 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
678 + 'The server used "renderToString" ' +
679 'which does not support Suspense. If you intended for this Suspense boundary to render ' +
680 'the fallback content on the server consider throwing an Error somewhere within the ' +
681 'Suspense boundary. If you intended to have the server wait for the suspended component ' +
@@ -719,7 +720,8 @@ describe('ReactDOMServerHydration', () => {
720 expect(errors.length).toBe(1);
721 if (__DEV__) {
722 expect(errors[0]).toBe(
722 - 'The server did not finish this Suspense boundary: The server used "renderToString" ' +
723 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
724 + 'The server used "renderToString" ' +
725 'which does not support Suspense. If you intended for this Suspense boundary to render ' +
726 'the fallback content on the server consider throwing an Error somewhere within the ' +
727 'Suspense boundary. If you intended to have the server wait for the suspended component ' +
packages/react-reconciler/src/ReactFiberBeginWork.js
+12 -6
@@ -2735,9 +2735,9 @@ function updateDehydratedSuspenseComponent(
2735 // get an update and we'll never be able to hydrate the final content. Let's just try the
2736 // client side render instead.
2737 let digest: ?string;
2738 - let message, stack;
2738 + let message, stack, componentStack;
2739 if (__DEV__) {
2740 - ({digest, message, stack} =
2740 + ({digest, message, stack, componentStack} =
2741 getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
2742 } else {
2743 ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
@@ -2747,18 +2747,24 @@ function updateDehydratedSuspenseComponent(
2747 // TODO: Figure out a better signal than encoding a magic digest value.
2748 if (!enablePostpone || digest !== 'POSTPONE') {
2749 let error;
2750 - if (message) {
2750 + if (__DEV__ && message) {
2751 // eslint-disable-next-line react-internal/prod-error-codes
2752 error = new Error(message);
2753 } else {
2754 error = new Error(
2755 'The server could not finish this Suspense boundary, likely ' +
2756 - 'due to an error during server rendering. Switched to ' +
2757 - 'client rendering.',
2756 + 'due to an error during server rendering. ' +
2757 + 'Switched to client rendering.',
2758 );
2759 }
2760 + // Replace the stack with the server stack
2761 + error.stack = (__DEV__ && stack) || '';
2762 (error: any).digest = digest;
2761 - capturedValue = createCapturedValueFromError(error, digest, stack);
2763 + capturedValue = createCapturedValueFromError(
2764 + error,
2765 + digest,
2766 + componentStack,
2767 + );
2768 }
2769 return retrySuspenseComponentWithoutHydrating(
2770 current,
packages/react-server/src/ReactFizzServer.js
+90 -50
@@ -203,9 +203,6 @@ const CLIENT_RENDERED = 4; // if it errors or infinitely suspends
203 type SuspenseBoundary = {
204 status: 0 | 1 | 4 | 5,
205 rootSegmentID: number,
206 - errorDigest: ?string, // the error hash if it errors
207 - errorMessage?: string, // the error string if it errors
208 - errorComponentStack?: string, // the error component stack if it errors
206 parentFlushed: boolean,
207 pendingTasks: number, // when it reaches zero we can show this boundary's content
208 completedSegments: Array<Segment>, // completed but not yet flushed segments.
@@ -215,6 +212,11 @@ type SuspenseBoundary = {
212 fallbackState: HoistableState,
213 trackedContentKeyPath: null | KeyNode, // used to track the path for replay nodes
214 trackedFallbackNode: null | ReplayNode, // used to track the fallback for replay nodes
215 + errorDigest: ?string, // the error hash if it errors
216 + // DEV-only fields
217 + errorMessage?: null | string, // the error string if it errors
218 + errorStack?: null | string, // the error stack if it errors
219 + errorComponentStack?: null | string, // the error component stack if it errors
220 };
221
222 type RenderTask = {
@@ -601,7 +603,7 @@ function createSuspenseBoundary(
603 request: Request,
604 fallbackAbortableTasks: Set<Task>,
605 ): SuspenseBoundary {
604 - return {
606 + const boundary: SuspenseBoundary = {
607 status: PENDING,
608 rootSegmentID: -1,
609 parentFlushed: false,
@@ -615,6 +617,13 @@ function createSuspenseBoundary(
617 trackedContentKeyPath: null,
618 trackedFallbackNode: null,
619 };
620 + if (__DEV__) {
621 + // DEV-only fields for hidden class
622 + boundary.errorMessage = null;
623 + boundary.errorStack = null;
624 + boundary.errorComponentStack = null;
625 + }
626 + return boundary;
627 }
628
629 function createRenderTask(
@@ -811,22 +820,30 @@ function encodeErrorForBoundary(
820 digest: ?string,
821 error: mixed,
822 thrownInfo: ThrownInfo,
823 + wasAborted: boolean,
824 ) {
825 boundary.errorDigest = digest;
826 if (__DEV__) {
817 - let message;
827 + let message, stack;
828 // In dev we additionally encode the error message and component stack on the boundary
829 if (error instanceof Error) {
830 // eslint-disable-next-line react-internal/safe-string-coercion
831 message = String(error.message);
832 + // eslint-disable-next-line react-internal/safe-string-coercion
833 + stack = String(error.stack);
834 } else if (typeof error === 'object' && error !== null) {
835 message = describeObjectForErrorMessage(error);
836 + stack = null;
837 } else {
838 // eslint-disable-next-line react-internal/safe-string-coercion
839 message = String(error);
840 + stack = null;
841 }
828 -
829 - boundary.errorMessage = message;
842 + const prefix = wasAborted
843 + ? 'Switched to client rendering because the server rendering aborted due to:\n\n'
844 + : 'Switched to client rendering because the server rendering errored:\n\n';
845 + boundary.errorMessage = prefix + message;
846 + boundary.errorStack = stack;
847 boundary.errorComponentStack = thrownInfo.componentStack;
848 }
849 }
@@ -1007,7 +1024,7 @@ function renderSuspenseBoundary(
1024 } else {
1025 errorDigest = logRecoverableError(request, error, thrownInfo);
1026 }
1010 - encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo);
1027 + encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo, false);
1028
1029 untrackBoundary(request, newBoundary);
1030
@@ -1151,7 +1168,13 @@ function replaySuspenseBoundary(
1168 } else {
1169 errorDigest = logRecoverableError(request, error, thrownInfo);
1170 }
1154 - encodeErrorForBoundary(resumedBoundary, errorDigest, error, thrownInfo);
1171 + encodeErrorForBoundary(
1172 + resumedBoundary,
1173 + errorDigest,
1174 + error,
1175 + thrownInfo,
1176 + false,
1177 + );
1178
1179 task.replay.pendingTasks--;
1180
@@ -2978,6 +3001,7 @@ function erroredReplay(
3001 error,
3002 errorDigest,
3003 errorInfo,
3004 + false,
3005 );
3006 }
3007
@@ -3008,7 +3032,7 @@ function erroredTask(
3032 boundary.pendingTasks--;
3033 if (boundary.status !== CLIENT_RENDERED) {
3034 boundary.status = CLIENT_RENDERED;
3011 - encodeErrorForBoundary(boundary, errorDigest, error, errorInfo);
3035 + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, false);
3036 untrackBoundary(request, boundary);
3037
3038 // Regardless of what happens next, this boundary won't be displayed,
@@ -3048,6 +3072,7 @@ function abortRemainingSuspenseBoundary(
3072 error: mixed,
3073 errorDigest: ?string,
3074 errorInfo: ThrownInfo,
3075 + wasAborted: boolean,
3076 ): void {
3077 const resumedBoundary = createSuspenseBoundary(request, new Set());
3078 resumedBoundary.parentFlushed = true;
@@ -3055,17 +3080,13 @@ function abortRemainingSuspenseBoundary(
3080 resumedBoundary.rootSegmentID = rootSegmentID;
3081
3082 resumedBoundary.status = CLIENT_RENDERED;
3058 - let errorMessage = error;
3059 - if (__DEV__) {
3060 - const errorPrefix = 'The server did not finish this Suspense boundary: ';
3061 - if (error && typeof error.message === 'string') {
3062 - errorMessage = errorPrefix + error.message;
3063 - } else {
3064 - // eslint-disable-next-line react-internal/safe-string-coercion
3065 - errorMessage = errorPrefix + String(error);
3066 - }
3067 - }
3068 - encodeErrorForBoundary(resumedBoundary, errorDigest, errorMessage, errorInfo);
3083 + encodeErrorForBoundary(
3084 + resumedBoundary,
3085 + errorDigest,
3086 + error,
3087 + errorInfo,
3088 + wasAborted,
3089 + );
3090
3091 if (resumedBoundary.parentFlushed) {
3092 request.clientRenderedBoundaries.push(resumedBoundary);
@@ -3080,6 +3101,7 @@ function abortRemainingReplayNodes(
3101 error: mixed,
3102 errorDigest: ?string,
3103 errorInfo: ThrownInfo,
3104 + aborted: boolean,
3105 ): void {
3106 for (let i = 0; i < nodes.length; i++) {
3107 const node = nodes[i];
@@ -3092,6 +3114,7 @@ function abortRemainingReplayNodes(
3114 error,
3115 errorDigest,
3116 errorInfo,
3117 + aborted,
3118 );
3119 } else {
3120 const boundaryNode: ReplaySuspenseBoundary = node;
@@ -3102,6 +3125,7 @@ function abortRemainingReplayNodes(
3125 error,
3126 errorDigest,
3127 errorInfo,
3128 + aborted,
3129 );
3130 }
3131 }
@@ -3118,7 +3142,7 @@ function abortRemainingReplayNodes(
3142 );
3143 } else if (boundary.status !== CLIENT_RENDERED) {
3144 boundary.status = CLIENT_RENDERED;
3121 - encodeErrorForBoundary(boundary, errorDigest, error, errorInfo);
3145 + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, aborted);
3146 if (boundary.parentFlushed) {
3147 request.clientRenderedBoundaries.push(boundary);
3148 }
@@ -3194,6 +3218,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3218 error,
3219 errorDigest,
3220 errorInfo,
3221 + true,
3222 );
3223 }
3224 request.pendingRootTasks--;
@@ -3223,18 +3248,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3248 } else {
3249 errorDigest = logRecoverableError(request, error, errorInfo);
3250 }
3226 - let errorMessage = error;
3227 - if (__DEV__) {
3228 - const errorPrefix =
3229 - 'The server did not finish this Suspense boundary: ';
3230 - if (error && typeof error.message === 'string') {
3231 - errorMessage = errorPrefix + error.message;
3232 - } else {
3233 - // eslint-disable-next-line react-internal/safe-string-coercion
3234 - errorMessage = errorPrefix + String(error);
3235 - }
3236 - }
3237 - encodeErrorForBoundary(boundary, errorDigest, errorMessage, errorInfo);
3251 + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true);
3252
3253 untrackBoundary(request, boundary);
3254
@@ -3757,13 +3771,25 @@ function flushSegment(
3771 // Emit a client rendered suspense boundary wrapper.
3772 // We never queue the inner boundary so we'll never emit its content or partial segments.
3773
3760 - writeStartClientRenderedSuspenseBoundary(
3761 - destination,
3762 - request.renderState,
3763 - boundary.errorDigest,
3764 - boundary.errorMessage,
3765 - boundary.errorComponentStack,
3766 - );
3774 + if (__DEV__) {
3775 + writeStartClientRenderedSuspenseBoundary(
3776 + destination,
3777 + request.renderState,
3778 + boundary.errorDigest,
3779 + boundary.errorMessage,
3780 + boundary.errorStack,
3781 + boundary.errorComponentStack,
3782 + );
3783 + } else {
3784 + writeStartClientRenderedSuspenseBoundary(
3785 + destination,
3786 + request.renderState,
3787 + boundary.errorDigest,
3788 + null,
3789 + null,
3790 + null,
3791 + );
3792 + }
3793 // Flush the fallback.
3794 flushSubtree(request, destination, segment, hoistableState);
3795
@@ -3849,15 +3875,29 @@ function flushClientRenderedBoundary(
3875 destination: Destination,
3876 boundary: SuspenseBoundary,
3877 ): boolean {
3852 - return writeClientRenderBoundaryInstruction(
3853 - destination,
3854 - request.resumableState,
3855 - request.renderState,
3856 - boundary.rootSegmentID,
3857 - boundary.errorDigest,
3858 - boundary.errorMessage,
3859 - boundary.errorComponentStack,
3860 - );
3878 + if (__DEV__) {
3879 + return writeClientRenderBoundaryInstruction(
3880 + destination,
3881 + request.resumableState,
3882 + request.renderState,
3883 + boundary.rootSegmentID,
3884 + boundary.errorDigest,
3885 + boundary.errorMessage,
3886 + boundary.errorStack,
3887 + boundary.errorComponentStack,
3888 + );
3889 + } else {
3890 + return writeClientRenderBoundaryInstruction(
3891 + destination,
3892 + request.resumableState,
3893 + request.renderState,
3894 + boundary.rootSegmentID,
3895 + boundary.errorDigest,
3896 + null,
3897 + null,
3898 + null,
3899 + );
3900 + }
3901 }
3902
3903 function flushSegmentContainer(
scripts/babel/transform-prevent-infinite-loops.js
+1 -1
@@ -13,7 +13,7 @@
13 // This should be reasonable for all loops in the source.
14 // Note that if the numbers are too large, the tests will take too long to fail
15 // for this to be useful (each individual test case might hit an infinite loop).
16 -const MAX_SOURCE_ITERATIONS = 1500;
16 +const MAX_SOURCE_ITERATIONS = 5000;
17 // Code in tests themselves is permitted to run longer.
18 // For example, in the fuzz tester.
19 const MAX_TEST_ITERATIONS = 5000;