diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 395392650904a..d67571fc41e1d 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -1226,15 +1226,21 @@ export function isSuspenseInstanceFallback( export function getSuspenseInstanceFallbackErrorDetails( instance: SuspenseInstance, -): {digest: ?string, message?: string, stack?: string} { +): { + digest: ?string, + message?: string, + stack?: string, + componentStack?: string, +} { const dataset = instance.nextSibling && ((instance.nextSibling: any): HTMLElement).dataset; - let digest, message, stack; + let digest, message, stack, componentStack; if (dataset) { digest = dataset.dgst; if (__DEV__) { message = dataset.msg; stack = dataset.stck; + componentStack = dataset.cstck; } } if (__DEV__) { @@ -1242,6 +1248,7 @@ export function getSuspenseInstanceFallbackErrorDetails( message, digest, stack, + componentStack, }; } else { // Object gets DCE'd if constructed in tail position and matches callsite destructuring diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js index da00da8e66437..b839575952598 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js @@ -94,6 +94,7 @@ function handleNode(node_: Node) { dataset['dgst'], dataset['msg'], dataset['stck'], + dataset['cstck'], ); node.remove(); } else if (dataset['rri'] != null) { diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 11a707348782b..58db005158618 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -3809,6 +3809,8 @@ const clientRenderedSuspenseBoundaryError1B = stringToPrecomputedChunk(' data-msg="'); const clientRenderedSuspenseBoundaryError1C = stringToPrecomputedChunk(' data-stck="'); +const clientRenderedSuspenseBoundaryError1D = + stringToPrecomputedChunk(' data-cstck="'); const clientRenderedSuspenseBoundaryError2 = stringToPrecomputedChunk('>'); @@ -3851,7 +3853,8 @@ export function writeStartClientRenderedSuspenseBoundary( destination: Destination, renderState: RenderState, errorDigest: ?string, - errorMesssage: ?string, + errorMessage: ?string, + errorStack: ?string, errorComponentStack: ?string, ): boolean { let result; @@ -3869,19 +3872,27 @@ export function writeStartClientRenderedSuspenseBoundary( ); } if (__DEV__) { - if (errorMesssage) { + if (errorMessage) { writeChunk(destination, clientRenderedSuspenseBoundaryError1B); writeChunk( destination, - stringToChunk(escapeTextForBrowser(errorMesssage)), + stringToChunk(escapeTextForBrowser(errorMessage)), ); writeChunk( destination, clientRenderedSuspenseBoundaryErrorAttrInterstitial, ); } - if (errorComponentStack) { + if (errorStack) { writeChunk(destination, clientRenderedSuspenseBoundaryError1C); + writeChunk(destination, stringToChunk(escapeTextForBrowser(errorStack))); + writeChunk( + destination, + clientRenderedSuspenseBoundaryErrorAttrInterstitial, + ); + } + if (errorComponentStack) { + writeChunk(destination, clientRenderedSuspenseBoundaryError1D); writeChunk( destination, stringToChunk(escapeTextForBrowser(errorComponentStack)), @@ -4244,6 +4255,7 @@ const clientRenderData1 = stringToPrecomputedChunk( const clientRenderData2 = stringToPrecomputedChunk('" data-dgst="'); const clientRenderData3 = stringToPrecomputedChunk('" data-msg="'); const clientRenderData4 = stringToPrecomputedChunk('" data-stck="'); +const clientRenderData5 = stringToPrecomputedChunk('" data-cstck="'); const clientRenderDataEnd = dataElementQuotedEnd; export function writeClientRenderBoundaryInstruction( @@ -4252,8 +4264,9 @@ export function writeClientRenderBoundaryInstruction( renderState: RenderState, id: number, errorDigest: ?string, - errorMessage?: string, - errorComponentStack?: string, + errorMessage: ?string, + errorStack: ?string, + errorComponentStack: ?string, ): boolean { const scriptFormat = !enableFizzExternalRuntime || @@ -4284,7 +4297,7 @@ export function writeClientRenderBoundaryInstruction( writeChunk(destination, clientRenderScript1A); } - if (errorDigest || errorMessage || errorComponentStack) { + if (errorDigest || errorMessage || errorStack || errorComponentStack) { if (scriptFormat) { // ,"JSONString" writeChunk(destination, clientRenderErrorScriptArgInterstitial); @@ -4301,7 +4314,7 @@ export function writeClientRenderBoundaryInstruction( ); } } - if (errorMessage || errorComponentStack) { + if (errorMessage || errorStack || errorComponentStack) { if (scriptFormat) { // ,"JSONString" writeChunk(destination, clientRenderErrorScriptArgInterstitial); @@ -4318,6 +4331,23 @@ export function writeClientRenderBoundaryInstruction( ); } } + if (errorStack || errorComponentStack) { + // ,"JSONString" + if (scriptFormat) { + writeChunk(destination, clientRenderErrorScriptArgInterstitial); + writeChunk( + destination, + stringToChunk(escapeJSStringsForInstructionScripts(errorStack || '')), + ); + } else { + // " data-stck="HTMLString + writeChunk(destination, clientRenderData4); + writeChunk( + destination, + stringToChunk(escapeTextForBrowser(errorStack || '')), + ); + } + } if (errorComponentStack) { // ,"JSONString" if (scriptFormat) { @@ -4329,8 +4359,8 @@ export function writeClientRenderBoundaryInstruction( ), ); } else { - // " data-stck="HTMLString - writeChunk(destination, clientRenderData4); + // " data-cstck="HTMLString + writeChunk(destination, clientRenderData5); writeChunk( destination, stringToChunk(escapeTextForBrowser(errorComponentStack)), diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index 251738aa2f0a5..92cd890976d22 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -219,6 +219,7 @@ export function writeStartClientRenderedSuspenseBoundary( // flushing these error arguments are not currently supported in this legacy streaming format. errorDigest: ?string, errorMessage: ?string, + errorStack: ?string, errorComponentStack: ?string, ): boolean { if (renderState.generateStaticMarkup) { @@ -231,6 +232,7 @@ export function writeStartClientRenderedSuspenseBoundary( renderState, errorDigest, errorMessage, + errorStack, errorComponentStack, ); } diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js index 042cfb23fefcd..1b0ba47378357 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js @@ -2,7 +2,7 @@ // The build script is at scripts/rollup/generate-inline-fizz-runtime.js. // Run `yarn generate-inline-fizz-runtime` to generate. export const clientRenderBoundary = - '$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())};'; + '$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())};'; export const completeBoundary = '$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()}};'; export const completeBoundaryWithStyles = diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js index a41c7d6f362ff..f9139094aa9b5 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js @@ -19,6 +19,7 @@ export function clientRenderBoundary( suspenseBoundaryID, errorDigest, errorMsg, + errorStack, errorComponentStack, ) { // Find the fallback's first element. @@ -36,7 +37,8 @@ export function clientRenderBoundary( const dataset = suspenseIdNode.dataset; if (errorDigest) dataset['dgst'] = errorDigest; if (errorMsg) dataset['msg'] = errorMsg; - if (errorComponentStack) dataset['stck'] = errorComponentStack; + if (errorStack) dataset['stck'] = errorStack; + if (errorComponentStack) dataset['cstck'] = errorComponentStack; // Tell React to retry it if the parent already hydrated. if (suspenseNode['_reactRetry']) { suspenseNode['_reactRetry'](); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 0d901ecc780a0..61ac405333c2a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -786,7 +786,8 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - theError.message, + 'Switched to client rendering because the server rendering errored:\n\n' + + theError.message, expectedDigest, componentStack(['Lazy', 'Suspense', 'div', 'App']), ], @@ -909,7 +910,8 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - theError.message, + 'Switched to client rendering because the server rendering errored:\n\n' + + theError.message, expectedDigest, componentStack(['Lazy', 'Suspense', 'div', 'App']), ], @@ -992,7 +994,8 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - theError.message, + 'Switched to client rendering because the server rendering errored:\n\n' + + theError.message, expectedDigest, componentStack([ 'Erroring', @@ -1078,7 +1081,8 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - theError.message, + 'Switched to client rendering because the server rendering errored:\n\n' + + theError.message, expectedDigest, componentStack(['Lazy', 'Suspense', 'div', 'App']), ], @@ -1404,13 +1408,15 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.', + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'The render was aborted by the server without a reason.', expectedDigest, // We get the stack of the task when it was aborted which is why we see `h1` componentStack(['h1', 'Suspense', 'div', 'App']), ], [ - 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.', + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'The render was aborted by the server without a reason.', expectedDigest, componentStack(['Suspense', 'main', 'div', 'App']), ], @@ -2145,7 +2151,8 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - theError.message, + 'Switched to client rendering because the server rendering errored:\n\n' + + theError.message, expectedDigest, componentStack([ 'AsyncText', @@ -3431,12 +3438,14 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - 'The server did not finish this Suspense boundary: foobar', + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'foobar', 'a digest', componentStack(['Suspense', 'p', 'div', 'App']), ], [ - 'The server did not finish this Suspense boundary: foobar', + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'foobar', 'a digest', componentStack(['Suspense', 'span', 'div', 'App']), ], @@ -3512,12 +3521,14 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - 'The server did not finish this Suspense boundary: uh oh', + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'uh oh', 'a digest', componentStack(['Suspense', 'p', 'div', 'App']), ], [ - 'The server did not finish this Suspense boundary: uh oh', + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'uh oh', 'a digest', componentStack(['Suspense', 'span', 'div', 'App']), ], @@ -3991,7 +4002,8 @@ describe('ReactDOMFizzServer', () => { errors, [ [ - theError.message, + 'Switched to client rendering because the server rendering errored:\n\n' + + theError.message, expectedDigest, componentStack(['Erroring', 'Suspense', 'div', 'App']), ], @@ -6772,7 +6784,14 @@ describe('ReactDOMFizzServer', () => { expect(recoverableErrors).toEqual( __DEV__ - ? ['server error', 'replay error', 'server error'] + ? [ + 'Switched to client rendering because the server rendering errored:\n\n' + + 'server error', + 'Switched to client rendering because the server rendering errored:\n\n' + + 'replay error', + 'Switched to client rendering because the server rendering errored:\n\n' + + 'server error', + ] : [ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', @@ -6931,8 +6950,10 @@ describe('ReactDOMFizzServer', () => { expect(recoverableErrors).toEqual( __DEV__ ? [ - 'The server did not finish this Suspense boundary: aborted', - 'The server did not finish this Suspense boundary: aborted', + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'aborted', + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'aborted', ] : [ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', @@ -7103,8 +7124,10 @@ describe('ReactDOMFizzServer', () => { // It surfaced in two different suspense boundaries. __DEV__ ? [ - 'The server did not finish this Suspense boundary: replay error', - 'The server did not finish this Suspense boundary: replay error', + 'Switched to client rendering because the server rendering errored:\n\n' + + 'replay error', + 'Switched to client rendering because the server rendering errored:\n\n' + + 'replay error', ] : [ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', @@ -7230,7 +7253,10 @@ describe('ReactDOMFizzServer', () => { expect(recoverableErrors).toEqual( __DEV__ - ? ['server error'] + ? [ + 'Switched to client rendering because the server rendering errored:\n\n' + + 'server error', + ] : [ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', ], diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index b973f85211a0a..07cbcbf9bed34 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -1292,10 +1292,12 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "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]", - ] - `); + [ + "Caught [Switched to client rendering because the server rendering aborted due to: + + 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]", + ] + `); }); // @gate __DEV__ @@ -1318,10 +1320,12 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "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]", - ] - `); + [ + "Caught [Switched to client rendering because the server rendering aborted due to: + + 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]", + ] + `); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 1ab7f81e4eaa2..89aa451a25008 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -2108,7 +2108,8 @@ describe('ReactDOMServerPartialHydration', () => { }); if (__DEV__) { await waitForAll([ - 'The server did not finish this Suspense boundary: The server used' + + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'The server used' + ' "renderToString" which does not support Suspense.', ]); } else { @@ -2177,7 +2178,8 @@ describe('ReactDOMServerPartialHydration', () => { }); if (__DEV__) { await waitForAll([ - 'The server did not finish this Suspense boundary: The server used' + + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'The server used' + ' "renderToString" which does not support Suspense.', ]); } else { @@ -2251,7 +2253,8 @@ describe('ReactDOMServerPartialHydration', () => { }); if (__DEV__) { await waitForAll([ - 'The server did not finish this Suspense boundary: The server used' + + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'The server used' + ' "renderToString" which does not support Suspense.', ]); } else { @@ -2571,7 +2574,8 @@ describe('ReactDOMServerPartialHydration', () => { suspend = true; if (__DEV__) { await waitForAll([ - 'The server did not finish this Suspense boundary: The server used' + + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'The server used' + ' "renderToString" which does not support Suspense.', ]); } else { @@ -2641,7 +2645,8 @@ describe('ReactDOMServerPartialHydration', () => { }); if (__DEV__) { await waitForAll([ - 'The server did not finish this Suspense boundary: The server used' + + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'The server used' + ' "renderToString" which does not support Suspense.', ]); } else { diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 3e878305048e5..c8829d6ebaced 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -681,7 +681,8 @@ describe('ReactDOMServerHydration', () => { expect(errors.length).toBe(1); if (__DEV__) { expect(errors[0]).toBe( - 'The server did not finish this Suspense boundary: The server used "renderToString" ' + + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + '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 ' + @@ -726,7 +727,8 @@ describe('ReactDOMServerHydration', () => { expect(errors.length).toBe(1); if (__DEV__) { expect(errors[0]).toBe( - 'The server did not finish this Suspense boundary: The server used "renderToString" ' + + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + '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 ' + diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 6e5f53edb796b..e5f14af35552d 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -2647,9 +2647,9 @@ function updateDehydratedSuspenseComponent( // get an update and we'll never be able to hydrate the final content. Let's just try the // client side render instead. let digest: ?string; - let message, stack; + let message, stack, componentStack; if (__DEV__) { - ({digest, message, stack} = + ({digest, message, stack, componentStack} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance)); } else { ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance)); @@ -2659,18 +2659,24 @@ function updateDehydratedSuspenseComponent( // TODO: Figure out a better signal than encoding a magic digest value. if (!enablePostpone || digest !== 'POSTPONE') { let error; - if (message) { + if (__DEV__ && message) { // eslint-disable-next-line react-internal/prod-error-codes error = new Error(message); } else { error = new Error( 'The server could not finish this Suspense boundary, likely ' + - 'due to an error during server rendering. Switched to ' + - 'client rendering.', + 'due to an error during server rendering. ' + + 'Switched to client rendering.', ); } + // Replace the stack with the server stack + error.stack = (__DEV__ && stack) || ''; (error: any).digest = digest; - capturedValue = createCapturedValueFromError(error, digest, stack); + capturedValue = createCapturedValueFromError( + error, + digest, + componentStack, + ); } return retrySuspenseComponentWithoutHydrating( current, diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 4567a1e80d13b..a06ef06e8bb2b 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -203,9 +203,6 @@ const CLIENT_RENDERED = 4; // if it errors or infinitely suspends type SuspenseBoundary = { status: 0 | 1 | 4 | 5, rootSegmentID: number, - errorDigest: ?string, // the error hash if it errors - errorMessage?: string, // the error string if it errors - errorComponentStack?: string, // the error component stack if it errors parentFlushed: boolean, pendingTasks: number, // when it reaches zero we can show this boundary's content completedSegments: Array, // completed but not yet flushed segments. @@ -215,6 +212,11 @@ type SuspenseBoundary = { fallbackState: HoistableState, trackedContentKeyPath: null | KeyNode, // used to track the path for replay nodes trackedFallbackNode: null | ReplayNode, // used to track the fallback for replay nodes + errorDigest: ?string, // the error hash if it errors + // DEV-only fields + errorMessage?: null | string, // the error string if it errors + errorStack?: null | string, // the error stack if it errors + errorComponentStack?: null | string, // the error component stack if it errors }; type RenderTask = { @@ -601,7 +603,7 @@ function createSuspenseBoundary( request: Request, fallbackAbortableTasks: Set, ): SuspenseBoundary { - return { + const boundary: SuspenseBoundary = { status: PENDING, rootSegmentID: -1, parentFlushed: false, @@ -615,6 +617,13 @@ function createSuspenseBoundary( trackedContentKeyPath: null, trackedFallbackNode: null, }; + if (__DEV__) { + // DEV-only fields for hidden class + boundary.errorMessage = null; + boundary.errorStack = null; + boundary.errorComponentStack = null; + } + return boundary; } function createRenderTask( @@ -811,22 +820,30 @@ function encodeErrorForBoundary( digest: ?string, error: mixed, thrownInfo: ThrownInfo, + wasAborted: boolean, ) { boundary.errorDigest = digest; if (__DEV__) { - let message; + let message, stack; // In dev we additionally encode the error message and component stack on the boundary if (error instanceof Error) { // eslint-disable-next-line react-internal/safe-string-coercion message = String(error.message); + // eslint-disable-next-line react-internal/safe-string-coercion + stack = String(error.stack); } else if (typeof error === 'object' && error !== null) { message = describeObjectForErrorMessage(error); + stack = null; } else { // eslint-disable-next-line react-internal/safe-string-coercion message = String(error); + stack = null; } - - boundary.errorMessage = message; + const prefix = wasAborted + ? 'Switched to client rendering because the server rendering aborted due to:\n\n' + : 'Switched to client rendering because the server rendering errored:\n\n'; + boundary.errorMessage = prefix + message; + boundary.errorStack = stack; boundary.errorComponentStack = thrownInfo.componentStack; } } @@ -1007,7 +1024,7 @@ function renderSuspenseBoundary( } else { errorDigest = logRecoverableError(request, error, thrownInfo); } - encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo); + encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo, false); untrackBoundary(request, newBoundary); @@ -1151,7 +1168,13 @@ function replaySuspenseBoundary( } else { errorDigest = logRecoverableError(request, error, thrownInfo); } - encodeErrorForBoundary(resumedBoundary, errorDigest, error, thrownInfo); + encodeErrorForBoundary( + resumedBoundary, + errorDigest, + error, + thrownInfo, + false, + ); task.replay.pendingTasks--; @@ -2948,6 +2971,7 @@ function erroredReplay( error, errorDigest, errorInfo, + false, ); } @@ -2978,7 +3002,7 @@ function erroredTask( boundary.pendingTasks--; if (boundary.status !== CLIENT_RENDERED) { boundary.status = CLIENT_RENDERED; - encodeErrorForBoundary(boundary, errorDigest, error, errorInfo); + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, false); untrackBoundary(request, boundary); // Regardless of what happens next, this boundary won't be displayed, @@ -3018,6 +3042,7 @@ function abortRemainingSuspenseBoundary( error: mixed, errorDigest: ?string, errorInfo: ThrownInfo, + wasAborted: boolean, ): void { const resumedBoundary = createSuspenseBoundary(request, new Set()); resumedBoundary.parentFlushed = true; @@ -3025,17 +3050,13 @@ function abortRemainingSuspenseBoundary( resumedBoundary.rootSegmentID = rootSegmentID; resumedBoundary.status = CLIENT_RENDERED; - let errorMessage = error; - if (__DEV__) { - const errorPrefix = 'The server did not finish this Suspense boundary: '; - if (error && typeof error.message === 'string') { - errorMessage = errorPrefix + error.message; - } else { - // eslint-disable-next-line react-internal/safe-string-coercion - errorMessage = errorPrefix + String(error); - } - } - encodeErrorForBoundary(resumedBoundary, errorDigest, errorMessage, errorInfo); + encodeErrorForBoundary( + resumedBoundary, + errorDigest, + error, + errorInfo, + wasAborted, + ); if (resumedBoundary.parentFlushed) { request.clientRenderedBoundaries.push(resumedBoundary); @@ -3050,6 +3071,7 @@ function abortRemainingReplayNodes( error: mixed, errorDigest: ?string, errorInfo: ThrownInfo, + aborted: boolean, ): void { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; @@ -3062,6 +3084,7 @@ function abortRemainingReplayNodes( error, errorDigest, errorInfo, + aborted, ); } else { const boundaryNode: ReplaySuspenseBoundary = node; @@ -3072,6 +3095,7 @@ function abortRemainingReplayNodes( error, errorDigest, errorInfo, + aborted, ); } } @@ -3088,7 +3112,7 @@ function abortRemainingReplayNodes( ); } else if (boundary.status !== CLIENT_RENDERED) { boundary.status = CLIENT_RENDERED; - encodeErrorForBoundary(boundary, errorDigest, error, errorInfo); + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, aborted); if (boundary.parentFlushed) { request.clientRenderedBoundaries.push(boundary); } @@ -3164,6 +3188,7 @@ function abortTask(task: Task, request: Request, error: mixed): void { error, errorDigest, errorInfo, + true, ); } request.pendingRootTasks--; @@ -3193,18 +3218,7 @@ function abortTask(task: Task, request: Request, error: mixed): void { } else { errorDigest = logRecoverableError(request, error, errorInfo); } - let errorMessage = error; - if (__DEV__) { - const errorPrefix = - 'The server did not finish this Suspense boundary: '; - if (error && typeof error.message === 'string') { - errorMessage = errorPrefix + error.message; - } else { - // eslint-disable-next-line react-internal/safe-string-coercion - errorMessage = errorPrefix + String(error); - } - } - encodeErrorForBoundary(boundary, errorDigest, errorMessage, errorInfo); + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true); untrackBoundary(request, boundary); @@ -3727,13 +3741,25 @@ function flushSegment( // Emit a client rendered suspense boundary wrapper. // We never queue the inner boundary so we'll never emit its content or partial segments. - writeStartClientRenderedSuspenseBoundary( - destination, - request.renderState, - boundary.errorDigest, - boundary.errorMessage, - boundary.errorComponentStack, - ); + if (__DEV__) { + writeStartClientRenderedSuspenseBoundary( + destination, + request.renderState, + boundary.errorDigest, + boundary.errorMessage, + boundary.errorStack, + boundary.errorComponentStack, + ); + } else { + writeStartClientRenderedSuspenseBoundary( + destination, + request.renderState, + boundary.errorDigest, + null, + null, + null, + ); + } // Flush the fallback. flushSubtree(request, destination, segment, hoistableState); @@ -3819,15 +3845,29 @@ function flushClientRenderedBoundary( destination: Destination, boundary: SuspenseBoundary, ): boolean { - return writeClientRenderBoundaryInstruction( - destination, - request.resumableState, - request.renderState, - boundary.rootSegmentID, - boundary.errorDigest, - boundary.errorMessage, - boundary.errorComponentStack, - ); + if (__DEV__) { + return writeClientRenderBoundaryInstruction( + destination, + request.resumableState, + request.renderState, + boundary.rootSegmentID, + boundary.errorDigest, + boundary.errorMessage, + boundary.errorStack, + boundary.errorComponentStack, + ); + } else { + return writeClientRenderBoundaryInstruction( + destination, + request.resumableState, + request.renderState, + boundary.rootSegmentID, + boundary.errorDigest, + null, + null, + null, + ); + } } function flushSegmentContainer( diff --git a/scripts/babel/transform-prevent-infinite-loops.js b/scripts/babel/transform-prevent-infinite-loops.js index 665569db27b7b..aa88377cc04c5 100644 --- a/scripts/babel/transform-prevent-infinite-loops.js +++ b/scripts/babel/transform-prevent-infinite-loops.js @@ -13,7 +13,7 @@ // This should be reasonable for all loops in the source. // Note that if the numbers are too large, the tests will take too long to fail // for this to be useful (each individual test case might hit an infinite loop). -const MAX_SOURCE_ITERATIONS = 1500; +const MAX_SOURCE_ITERATIONS = 5000; // Code in tests themselves is permitted to run longer. // For example, in the fuzz tester. const MAX_TEST_ITERATIONS = 5000;