diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 66ae2c25e690..7104350fde00 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -90,46 +90,28 @@ describe('ReactDOMFizzServer', () => { }); function expectErrors(errorsArr, toBeDevArr, toBeProdArr) { - const mappedErrows = errorsArr.map(error => { - if (error.componentStack) { - return [ - error.message, - error.hash, - normalizeCodeLocInfo(error.componentStack), - ]; - } else if (error.hash) { - return [error.message, error.hash]; + const mappedErrows = errorsArr.map(({error, errorInfo}) => { + const stack = errorInfo && errorInfo.componentStack; + const digest = errorInfo && errorInfo.digest; + if (stack) { + return [error.message, digest, normalizeCodeLocInfo(stack)]; + } else if (digest) { + return [error.message, digest]; } return error.message; }); if (__DEV__) { - expect(mappedErrows).toEqual( - toBeDevArr, - // .map(([errorMessage, errorHash, errorComponentStack]) => { - // if (typeof error === 'string' || error instanceof String) { - // return error; - // } - // let str = JSON.stringify(error).replace(/\\n/g, '\n'); - // // this gets stripped away by normalizeCodeLocInfo... - // // Kind of hacky but lets strip it away here too just so they match... - // // easier than fixing the regex to account for this edge case - // if (str.endsWith('at **)"}')) { - // str = str.replace(/at \*\*\)\"}$/, 'at **)'); - // } - // return str; - // }), - ); + expect(mappedErrows).toEqual(toBeDevArr); } else { expect(mappedErrows).toEqual(toBeProdArr); } } - // @TODO we will use this in a followup change once we start exposing componentStacks from server errors - // function componentStack(components) { - // return components - // .map(component => `\n in ${component} (at **)`) - // .join(''); - // } + function componentStack(components) { + return components + .map(component => `\n in ${component} (at **)`) + .join(''); + } async function act(callback) { await callback(); @@ -471,8 +453,8 @@ describe('ReactDOMFizzServer', () => { bootstrapped = true; // Attempt to hydrate the content. ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - errors.push(error); + onRecoverableError(error, errorInfo) { + errors.push({error, errorInfo}); }, }); }; @@ -483,8 +465,8 @@ describe('ReactDOMFizzServer', () => { loggedErrors.push(x); return 'Hash of (' + x.message + ')'; } - // const expectedHash = onError(theError); - // loggedErrors.length = 0; + const expectedDigest = onError(theError); + loggedErrors.length = 0; await act(async () => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( @@ -519,9 +501,18 @@ describe('ReactDOMFizzServer', () => { expect(Scheduler).toFlushAndYield([]); expectErrors( errors, - [theError.message], [ - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + [ + theError.message, + expectedDigest, + componentStack(['Lazy', 'Suspense', 'div', 'App']), + ], + ], + [ + [ + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + expectedDigest, + ], ], ); @@ -577,8 +568,8 @@ describe('ReactDOMFizzServer', () => { loggedErrors.push(x); return 'hash of (' + x.message + ')'; } - // const expectedHash = onError(theError); - // loggedErrors.length = 0; + const expectedDigest = onError(theError); + loggedErrors.length = 0; function App({isClient}) { return ( @@ -605,8 +596,8 @@ describe('ReactDOMFizzServer', () => { const errors = []; // Attempt to hydrate the content. ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - errors.push(error); + onRecoverableError(error, errorInfo) { + errors.push({error, errorInfo}); }, }); Scheduler.unstable_flushAll(); @@ -630,9 +621,18 @@ describe('ReactDOMFizzServer', () => { expectErrors( errors, - [theError.message], [ - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + [ + theError.message, + expectedDigest, + componentStack(['Suspense', 'div', 'App']), + ], + ], + [ + [ + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + expectedDigest, + ], ], ); @@ -675,8 +675,8 @@ describe('ReactDOMFizzServer', () => { loggedErrors.push(x); return 'hash(' + x.message + ')'; } - // const expectedHash = onError(theError); - // loggedErrors.length = 0; + const expectedDigest = onError(theError); + loggedErrors.length = 0; await act(async () => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( @@ -693,8 +693,8 @@ describe('ReactDOMFizzServer', () => { const errors = []; // Attempt to hydrate the content. ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - errors.push(error); + onRecoverableError(error, errorInfo) { + errors.push({error, errorInfo}); }, }); Scheduler.unstable_flushAll(); @@ -703,9 +703,18 @@ describe('ReactDOMFizzServer', () => { expectErrors( errors, - [theError.message], [ - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + [ + theError.message, + expectedDigest, + componentStack(['Erroring', 'Suspense', 'div', 'App']), + ], + ], + [ + [ + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + expectedDigest, + ], ], ); }); @@ -735,8 +744,8 @@ describe('ReactDOMFizzServer', () => { loggedErrors.push(x); return 'hash(' + x.message + ')'; } - // const expectedHash = onError(theError); - // loggedErrors.length = 0; + const expectedDigest = onError(theError); + loggedErrors.length = 0; await act(async () => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( @@ -753,8 +762,8 @@ describe('ReactDOMFizzServer', () => { const errors = []; // Attempt to hydrate the content. ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - errors.push(error); + onRecoverableError(error, errorInfo) { + errors.push({error, errorInfo}); }, }); Scheduler.unstable_flushAll(); @@ -773,9 +782,18 @@ describe('ReactDOMFizzServer', () => { expectErrors( errors, - [theError.message], [ - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + [ + theError.message, + expectedDigest, + componentStack(['Lazy', 'Suspense', 'div', 'App']), + ], + ], + [ + [ + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + expectedDigest, + ], ], ); @@ -1053,9 +1071,10 @@ describe('ReactDOMFizzServer', () => { } const loggedErrors = []; + const expectedDigest = 'Hash for Abort'; function onError(error) { loggedErrors.push(error); - return `Hash of (${error.message})`; + return expectedDigest; } let controls; @@ -1069,8 +1088,8 @@ describe('ReactDOMFizzServer', () => { const errors = []; // Attempt to hydrate the content. ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - errors.push(error); + onRecoverableError(error, errorInfo) { + errors.push({error, errorInfo}); }, }); Scheduler.unstable_flushAll(); @@ -1087,9 +1106,12 @@ describe('ReactDOMFizzServer', () => { expect(Scheduler).toFlushAndYield([]); expectErrors( errors, - ['This Suspense boundary was aborted by the server'], + [['This Suspense boundary was aborted by the server.', expectedDigest]], [ - '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.', + expectedDigest, + ], ], ); expect(getVisibleChildren(container)).toEqual(
Loading...
); @@ -1755,8 +1777,8 @@ describe('ReactDOMFizzServer', () => { loggedErrors.push(x); return `hash of (${x.message})`; } - // const expectedHash = onError(theError); - // loggedErrors.length = 0; + const expectedDigest = onError(theError); + loggedErrors.length = 0; let controls; await act(async () => { @@ -1775,8 +1797,8 @@ describe('ReactDOMFizzServer', () => { const errors = []; // Attempt to hydrate the content. ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - errors.push(error); + onRecoverableError(error, errorInfo) { + errors.push({error, errorInfo}); }, }); Scheduler.unstable_flushAll(); @@ -1809,9 +1831,25 @@ describe('ReactDOMFizzServer', () => { expect(Scheduler).toFlushAndYield([]); expectErrors( errors, - [theError.message], [ - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + [ + theError.message, + expectedDigest, + componentStack([ + 'AsyncText', + 'h1', + 'Suspense', + 'div', + 'Suspense', + 'App', + ]), + ], + ], + [ + [ + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + expectedDigest, + ], ], ); @@ -3142,8 +3180,8 @@ describe('ReactDOMFizzServer', () => { loggedErrors.push(x); return x.message.replace('bad message', 'bad hash'); } - // const expectedHash = onError(theError); - // loggedErrors.length = 0; + const expectedDigest = onError(theError); + loggedErrors.length = 0; await act(async () => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream(, { @@ -3156,8 +3194,8 @@ describe('ReactDOMFizzServer', () => { const errors = []; ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - errors.push(error); + onRecoverableError(error, errorInfo) { + errors.push({error, errorInfo}); }, }); expect(Scheduler).toFlushAndYield([]); @@ -3165,9 +3203,18 @@ describe('ReactDOMFizzServer', () => { // If escaping were not done we would get a message that says "bad hash" expectErrors( errors, - [theError.message], [ - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + [ + theError.message, + expectedDigest, + componentStack(['Erroring', 'Suspense', 'div', 'App']), + ], + ], + [ + [ + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + expectedDigest, + ], ], ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js index 3aacf68cc3b2..a1429b0d2a17 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js @@ -215,7 +215,7 @@ describe('ReactDOMFizzServer', () => { expect(result).toContain('Loading'); expect(errors).toEqual([ - 'This Suspense boundary was aborted by the server', + 'This Suspense boundary was aborted by the server.', ]); }); @@ -256,7 +256,7 @@ describe('ReactDOMFizzServer', () => { reader.cancel(); expect(errors).toEqual([ - 'This Suspense boundary was aborted by the server', + 'This Suspense boundary was aborted by the server.', ]); hasLoaded = true; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index cb5ec892bd2c..a625a8df0e2f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -226,7 +226,7 @@ describe('ReactDOMFizzServer', () => { expect(output.result).toBe(''); expect(reportedErrors).toEqual([ theError.message, - 'This Suspense boundary was aborted by the server', + 'This Suspense boundary was aborted by the server.', ]); expect(reportedShellErrors).toEqual([theError]); }); @@ -322,7 +322,7 @@ describe('ReactDOMFizzServer', () => { await completed; expect(errors).toEqual([ - 'This Suspense boundary was aborted by the server', + 'This Suspense boundary was aborted by the server.', ]); expect(output.error).toBe(undefined); expect(output.result).toContain('Loading'); @@ -365,8 +365,8 @@ describe('ReactDOMFizzServer', () => { expect(errors).toEqual([ // There are two boundaries that abort - 'This Suspense boundary was aborted by the server', - 'This Suspense boundary was aborted by the server', + 'This Suspense boundary was aborted by the server.', + 'This Suspense boundary was aborted by the server.', ]); expect(output.error).toBe(undefined); expect(output.result).toContain('Loading'); @@ -603,7 +603,7 @@ describe('ReactDOMFizzServer', () => { await completed; expect(errors).toEqual([ - 'This Suspense boundary was aborted by the server', + 'This Suspense boundary was aborted by the server.', ]); expect(rendered).toBe(false); expect(isComplete).toBe(true); diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index f6b5a0b428a7..f2ddab69cdd0 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -11,6 +11,7 @@ let React; let ReactDOMClient; let ReactDOMServer; let act; +let usingPartialRenderer; const util = require('util'); const realConsoleError = console.error; @@ -25,6 +26,8 @@ describe('ReactDOMServerHydration', () => { ReactDOMServer = require('react-dom/server'); act = require('react-dom/test-utils').act; + usingPartialRenderer = global.__WWW__ && !__EXPERIMENTAL__; + console.error = jest.fn(); container = document.createElement('div'); document.body.appendChild(container); @@ -727,9 +730,16 @@ describe('ReactDOMServerHydration', () => { ); } + + // @TODO FB bundles use a different renderer that does not serialize errors to the client + const mismatchEl = usingPartialRenderer ? '

' : '', ); export function pushStartCompletedSuspenseBoundary( @@ -1576,7 +1582,7 @@ export function writeStartPendingSuspenseBoundary( export function writeStartClientRenderedSuspenseBoundary( destination: Destination, responseState: ResponseState, - errorHash: ?string, + errorDigest: ?string, errorMesssage: ?string, errorComponentStack: ?string, ): boolean { @@ -1585,33 +1591,43 @@ export function writeStartClientRenderedSuspenseBoundary( destination, startClientRenderedSuspenseBoundary, ); - if (errorHash) { - writeChunk(destination, clientRenderedSuspenseBoundaryError1); - writeChunk(destination, stringToChunk(escapeTextForBrowser(errorHash))); - // In prod errorMessage will usually be nullish but there is one case where - // it is used (currently when the server aborts the task) so we leave it ungated. + writeChunk(destination, clientRenderedSuspenseBoundaryError1); + if (errorDigest) { + writeChunk(destination, clientRenderedSuspenseBoundaryError1A); + writeChunk(destination, stringToChunk(escapeTextForBrowser(errorDigest))); + writeChunk( + destination, + clientRenderedSuspenseBoundaryErrorAttrInterstitial, + ); + } + if (__DEV__) { if (errorMesssage) { - writeChunk(destination, clientRenderedSuspenseBoundaryError1A); + writeChunk(destination, clientRenderedSuspenseBoundaryError1B); writeChunk( destination, stringToChunk(escapeTextForBrowser(errorMesssage)), ); + writeChunk( + destination, + clientRenderedSuspenseBoundaryErrorAttrInterstitial, + ); } - if (__DEV__) { - // Component stacks are currently only captured in dev - if (errorComponentStack) { - writeChunk(destination, clientRenderedSuspenseBoundaryError1B); - writeChunk( - destination, - stringToChunk(escapeTextForBrowser(errorComponentStack)), - ); - } + if (errorComponentStack) { + writeChunk(destination, clientRenderedSuspenseBoundaryError1C); + writeChunk( + destination, + stringToChunk(escapeTextForBrowser(errorComponentStack)), + ); + writeChunk( + destination, + clientRenderedSuspenseBoundaryErrorAttrInterstitial, + ); } - result = writeChunkAndReturn( - destination, - clientRenderedSuspenseBoundaryError2, - ); } + result = writeChunkAndReturn( + destination, + clientRenderedSuspenseBoundaryError2, + ); return result; } export function writeEndCompletedSuspenseBoundary( @@ -1772,7 +1788,7 @@ export function writeEndSegment( // const SUSPENSE_PENDING_START_DATA = '$?'; // const SUSPENSE_FALLBACK_START_DATA = '$!'; // -// function clientRenderBoundary(suspenseBoundaryID, errorHash, errorMsg, errorComponentStack) { +// function clientRenderBoundary(suspenseBoundaryID, errorDigest, errorMsg, errorComponentStack) { // // Find the fallback's first element. // const suspenseIdNode = document.getElementById(suspenseBoundaryID); // if (!suspenseIdNode) { @@ -1786,9 +1802,9 @@ export function writeEndSegment( // suspenseNode.data = SUSPENSE_FALLBACK_START_DATA; // // assign error metadata to first sibling // let dataset = suspenseIdNode.dataset; -// if (errorHash) dataset.hash = errorHash; +// if (errorDigest) dataset.dgst = errorDigest; // if (errorMsg) dataset.msg = errorMsg; -// if (errorComponentStack) dataset.stack = errorComponentStack; +// if (errorComponentStack) dataset.stck = errorComponentStack; // // Tell React to retry it if the parent already hydrated. // if (suspenseNode._reactRetry) { // suspenseNode._reactRetry(); @@ -1876,7 +1892,7 @@ const completeSegmentFunction = const completeBoundaryFunction = '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()}}'; const clientRenderFunction = - '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())}'; + '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())}'; const completeSegmentScript1Full = stringToPrecomputedChunk( completeSegmentFunction + ';$RS("', @@ -1957,7 +1973,7 @@ export function writeClientRenderBoundaryInstruction( destination: Destination, responseState: ResponseState, boundaryID: SuspenseBoundaryID, - errorHash: ?string, + errorDigest: ?string, errorMessage?: string, errorComponentStack?: string, ): boolean { @@ -1979,11 +1995,11 @@ export function writeClientRenderBoundaryInstruction( writeChunk(destination, boundaryID); writeChunk(destination, clientRenderScript1A); - if (errorHash || errorMessage || errorComponentStack) { + if (errorDigest || errorMessage || errorComponentStack) { writeChunk(destination, clientRenderErrorScriptArgInterstitial); writeChunk( destination, - stringToChunk(escapeJSStringsForInstructionScripts(errorHash || '')), + stringToChunk(escapeJSStringsForInstructionScripts(errorDigest || '')), ); } if (errorMessage || errorComponentStack) { diff --git a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js index 71716d7b71f2..375562e80b56 100644 --- a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js @@ -149,9 +149,9 @@ export function writeStartClientRenderedSuspenseBoundary( destination: Destination, responseState: ResponseState, // flushing these error arguments are not currently supported in this legacy streaming format. - errorHash: ?string, - errorMessage?: string, - errorComponentStack?: string, + errorDigest: ?string, + errorMessage: ?string, + errorComponentStack: ?string, ): boolean { if (responseState.generateStaticMarkup) { // A client rendered boundary is done and doesn't need a representation in the HTML @@ -161,6 +161,9 @@ export function writeStartClientRenderedSuspenseBoundary( return writeStartClientRenderedSuspenseBoundaryImpl( destination, responseState, + errorDigest, + errorMessage, + errorComponentStack, ); } export function writeEndCompletedSuspenseBoundary( diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js index d65a30180a4d..3c2c23c911fa 100644 --- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js +++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js @@ -226,7 +226,7 @@ export function writeStartClientRenderedSuspenseBoundary( destination: Destination, responseState: ResponseState, // TODO: encode error for native - errorHash: ?string, + errorDigest: ?string, errorMessage: ?string, errorComponentStack: ?string, ): boolean { @@ -300,7 +300,7 @@ export function writeClientRenderBoundaryInstruction( responseState: ResponseState, boundaryID: SuspenseBoundaryID, // TODO: encode error for native - errorHash: ?string, + errorDigest: ?string, errorMessage: ?string, errorComponentStack: ?string, ): boolean { diff --git a/packages/react-reconciler/src/ReactCapturedValue.js b/packages/react-reconciler/src/ReactCapturedValue.js index 8b87eb9a6c1b..64b1f18515d5 100644 --- a/packages/react-reconciler/src/ReactCapturedValue.js +++ b/packages/react-reconciler/src/ReactCapturedValue.js @@ -15,9 +15,10 @@ export type CapturedValue = {| value: T, source: Fiber | null, stack: string | null, + digest: string | null, |}; -export function createCapturedValue( +export function createCapturedValueAtFiber( value: T, source: Fiber, ): CapturedValue { @@ -27,5 +28,19 @@ export function createCapturedValue( value, source, stack: getStackByFiberInDevAndProd(source), + digest: null, + }; +} + +export function createCapturedValue( + value: T, + digest: ?string, + stack: ?string, +): CapturedValue { + return { + value, + source: null, + stack: stack != null ? stack : null, + digest: digest != null ? digest : null, }; } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 4f227ed4710c..aa7ec7c5f9bd 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -237,7 +237,11 @@ import { import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.new'; import {setWorkInProgressVersion} from './ReactMutableSource.new'; import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.new'; -import {createCapturedValue} from './ReactCapturedValue'; +import { + createCapturedValue, + createCapturedValueAtFiber, + type CapturedValue, +} from './ReactCapturedValue'; import {createClassErrorUpdate} from './ReactFiberThrow.new'; import is from 'shared/objectIs'; import { @@ -1074,7 +1078,7 @@ function updateClassComponent( // Schedule the error boundary to re-render using updated state const update = createClassErrorUpdate( workInProgress, - createCapturedValue(error, workInProgress), + createCapturedValueAtFiber(error, workInProgress), lane, ); enqueueCapturedUpdate(workInProgress, update); @@ -1322,10 +1326,13 @@ function updateHostRoot(current, workInProgress, renderLanes) { if (workInProgress.flags & ForceClientRender) { // Something errored during a previous attempt to hydrate the shell, so we // forced a client render. - const recoverableError = new Error( - 'There was an error while hydrating. Because the error happened outside ' + - 'of a Suspense boundary, the entire root will switch to ' + - 'client rendering.', + const recoverableError = createCapturedValueAtFiber( + new Error( + 'There was an error while hydrating. Because the error happened outside ' + + 'of a Suspense boundary, the entire root will switch to ' + + 'client rendering.', + ), + workInProgress, ); return mountHostRootWithoutHydrating( current, @@ -1335,9 +1342,12 @@ function updateHostRoot(current, workInProgress, renderLanes) { recoverableError, ); } else if (nextChildren !== prevChildren) { - const recoverableError = new Error( - 'This root received an early update, before anything was able ' + - 'hydrate. Switched the entire root to client rendering.', + const recoverableError = createCapturedValueAtFiber( + new Error( + 'This root received an early update, before anything was able ' + + 'hydrate. Switched the entire root to client rendering.', + ), + workInProgress, ); return mountHostRootWithoutHydrating( current, @@ -1400,7 +1410,7 @@ function mountHostRootWithoutHydrating( workInProgress: Fiber, nextChildren: ReactNodeList, renderLanes: Lanes, - recoverableError: Error, + recoverableError: CapturedValue, ) { // Revert to client rendering. resetHydrationState(); @@ -2429,7 +2439,7 @@ function retrySuspenseComponentWithoutHydrating( current: Fiber, workInProgress: Fiber, renderLanes: Lanes, - recoverableError: Error | null, + recoverableError: CapturedValue | null, ) { // Falling back to client rendering. Because this has performance // implications, it's considered a recoverable error, even though the user @@ -2574,22 +2584,29 @@ function updateDehydratedSuspenseComponent( // This boundary is in a permanent fallback state. In this case, we'll never // get an update and we'll never be able to hydrate the final content. Let's just try the // client side render instead. - const {errorMessage} = getSuspenseInstanceFallbackErrorDetails( - suspenseInstance, - ); - const error = errorMessage + let digest, message, stack; + if (__DEV__) { + ({digest, message, stack} = getSuspenseInstanceFallbackErrorDetails( + suspenseInstance, + )); + } else { + ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance)); + } + + const error = message ? // eslint-disable-next-line react-internal/prod-error-codes - new Error(errorMessage) + new Error(message) : new Error( 'The server could not finish this Suspense boundary, likely ' + 'due to an error during server rendering. Switched to ' + 'client rendering.', ); + const capturedValue = createCapturedValue(error, digest, stack); return retrySuspenseComponentWithoutHydrating( current, workInProgress, renderLanes, - error, + capturedValue, ); } @@ -2650,10 +2667,7 @@ function updateDehydratedSuspenseComponent( // skip hydration. // Delay having to do this as long as the suspense timeout allows us. renderDidSuspendDelayIfPossible(); - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, + const capturedValue = createCapturedValue( new Error( 'This Suspense boundary received an update before it finished ' + 'hydrating. This caused the boundary to switch to client rendering. ' + @@ -2661,6 +2675,12 @@ function updateDehydratedSuspenseComponent( 'in startTransition.', ), ); + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderLanes, + capturedValue, + ); } else if (isSuspenseInstancePending(suspenseInstance)) { // This component is still pending more data from the server, so we can't hydrate its // content. We treat it as if this component suspended itself. It might seem as if @@ -2707,15 +2727,18 @@ function updateDehydratedSuspenseComponent( if (workInProgress.flags & ForceClientRender) { // Something errored during hydration. Try again without hydrating. workInProgress.flags &= ~ForceClientRender; - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, + const capturedValue = createCapturedValue( new Error( 'There was an error while hydrating this Suspense boundary. ' + 'Switched to client rendering.', ), ); + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderLanes, + capturedValue, + ); } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) { // Something suspended and we should still be in dehydrated mode. // Leave the existing child in place. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 63a8ae2212c7..4601d0dded1c 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -237,7 +237,11 @@ import { import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old'; import {setWorkInProgressVersion} from './ReactMutableSource.old'; import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.old'; -import {createCapturedValue} from './ReactCapturedValue'; +import { + createCapturedValue, + createCapturedValueAtFiber, + type CapturedValue, +} from './ReactCapturedValue'; import {createClassErrorUpdate} from './ReactFiberThrow.old'; import is from 'shared/objectIs'; import { @@ -1074,7 +1078,7 @@ function updateClassComponent( // Schedule the error boundary to re-render using updated state const update = createClassErrorUpdate( workInProgress, - createCapturedValue(error, workInProgress), + createCapturedValueAtFiber(error, workInProgress), lane, ); enqueueCapturedUpdate(workInProgress, update); @@ -1322,10 +1326,13 @@ function updateHostRoot(current, workInProgress, renderLanes) { if (workInProgress.flags & ForceClientRender) { // Something errored during a previous attempt to hydrate the shell, so we // forced a client render. - const recoverableError = new Error( - 'There was an error while hydrating. Because the error happened outside ' + - 'of a Suspense boundary, the entire root will switch to ' + - 'client rendering.', + const recoverableError = createCapturedValueAtFiber( + new Error( + 'There was an error while hydrating. Because the error happened outside ' + + 'of a Suspense boundary, the entire root will switch to ' + + 'client rendering.', + ), + workInProgress, ); return mountHostRootWithoutHydrating( current, @@ -1335,9 +1342,12 @@ function updateHostRoot(current, workInProgress, renderLanes) { recoverableError, ); } else if (nextChildren !== prevChildren) { - const recoverableError = new Error( - 'This root received an early update, before anything was able ' + - 'hydrate. Switched the entire root to client rendering.', + const recoverableError = createCapturedValueAtFiber( + new Error( + 'This root received an early update, before anything was able ' + + 'hydrate. Switched the entire root to client rendering.', + ), + workInProgress, ); return mountHostRootWithoutHydrating( current, @@ -1400,7 +1410,7 @@ function mountHostRootWithoutHydrating( workInProgress: Fiber, nextChildren: ReactNodeList, renderLanes: Lanes, - recoverableError: Error, + recoverableError: CapturedValue, ) { // Revert to client rendering. resetHydrationState(); @@ -2429,7 +2439,7 @@ function retrySuspenseComponentWithoutHydrating( current: Fiber, workInProgress: Fiber, renderLanes: Lanes, - recoverableError: Error | null, + recoverableError: CapturedValue | null, ) { // Falling back to client rendering. Because this has performance // implications, it's considered a recoverable error, even though the user @@ -2574,22 +2584,29 @@ function updateDehydratedSuspenseComponent( // This boundary is in a permanent fallback state. In this case, we'll never // get an update and we'll never be able to hydrate the final content. Let's just try the // client side render instead. - const {errorMessage} = getSuspenseInstanceFallbackErrorDetails( - suspenseInstance, - ); - const error = errorMessage + let digest, message, stack; + if (__DEV__) { + ({digest, message, stack} = getSuspenseInstanceFallbackErrorDetails( + suspenseInstance, + )); + } else { + ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance)); + } + + const error = message ? // eslint-disable-next-line react-internal/prod-error-codes - new Error(errorMessage) + new Error(message) : new Error( 'The server could not finish this Suspense boundary, likely ' + 'due to an error during server rendering. Switched to ' + 'client rendering.', ); + const capturedValue = createCapturedValue(error, digest, stack); return retrySuspenseComponentWithoutHydrating( current, workInProgress, renderLanes, - error, + capturedValue, ); } @@ -2650,10 +2667,7 @@ function updateDehydratedSuspenseComponent( // skip hydration. // Delay having to do this as long as the suspense timeout allows us. renderDidSuspendDelayIfPossible(); - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, + const capturedValue = createCapturedValue( new Error( 'This Suspense boundary received an update before it finished ' + 'hydrating. This caused the boundary to switch to client rendering. ' + @@ -2661,6 +2675,12 @@ function updateDehydratedSuspenseComponent( 'in startTransition.', ), ); + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderLanes, + capturedValue, + ); } else if (isSuspenseInstancePending(suspenseInstance)) { // This component is still pending more data from the server, so we can't hydrate its // content. We treat it as if this component suspended itself. It might seem as if @@ -2707,15 +2727,18 @@ function updateDehydratedSuspenseComponent( if (workInProgress.flags & ForceClientRender) { // Something errored during hydration. Try again without hydrating. workInProgress.flags &= ~ForceClientRender; - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, + const capturedValue = createCapturedValue( new Error( 'There was an error while hydrating this Suspense boundary. ' + 'Switched to client rendering.', ), ); + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderLanes, + capturedValue, + ); } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) { // Something suspended and we should still be in dehydrated mode. // Leave the existing child in place. diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index 636a467475df..fc18efab3716 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -19,6 +19,7 @@ import type { } from './ReactFiberHostConfig'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {TreeContext} from './ReactFiberTreeContext.new'; +import type {CapturedValue} from './ReactCapturedValue'; import { HostComponent, @@ -86,7 +87,7 @@ let isHydrating: boolean = false; let didSuspendOrErrorDEV: boolean = false; // Hydration errors that were thrown inside this boundary -let hydrationErrors: Array | null = null; +let hydrationErrors: Array> | null = null; function warnIfHydrating() { if (__DEV__) { @@ -680,7 +681,7 @@ function getIsHydrating(): boolean { return isHydrating; } -export function queueHydrationError(error: mixed): void { +export function queueHydrationError(error: CapturedValue): void { if (hydrationErrors === null) { hydrationErrors = [error]; } else { diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js index 3befb348c05a..099b02fbcecc 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js @@ -19,6 +19,7 @@ import type { } from './ReactFiberHostConfig'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; import type {TreeContext} from './ReactFiberTreeContext.old'; +import type {CapturedValue} from './ReactCapturedValue'; import { HostComponent, @@ -86,7 +87,7 @@ let isHydrating: boolean = false; let didSuspendOrErrorDEV: boolean = false; // Hydration errors that were thrown inside this boundary -let hydrationErrors: Array | null = null; +let hydrationErrors: Array> | null = null; function warnIfHydrating() { if (__DEV__) { @@ -680,7 +681,7 @@ function getIsHydrating(): boolean { return isHydrating; } -export function queueHydrationError(error: mixed): void { +export function queueHydrationError(error: CapturedValue): void { if (hydrationErrors === null) { hydrationErrors = [error]; } else { diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index 3d13cd6407b4..f3dc2edf00f0 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -41,7 +41,7 @@ import { enableLazyContextPropagation, enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; -import {createCapturedValue} from './ReactCapturedValue'; +import {createCapturedValueAtFiber} from './ReactCapturedValue'; import { enqueueCapturedUpdate, createUpdate, @@ -517,7 +517,7 @@ function throwException( // Even though the user may not be affected by this error, we should // still log it so it can be fixed. - queueHydrationError(value); + queueHydrationError(createCapturedValueAtFiber(value, sourceFiber)); return; } } else { @@ -525,12 +525,12 @@ function throwException( } } + value = createCapturedValueAtFiber(value, sourceFiber); + renderDidError(value); + // We didn't find a boundary that could handle this type of exception. Start // over and traverse parent path again, this time treating the exception // as an error. - renderDidError(value); - - value = createCapturedValue(value, sourceFiber); let workInProgress = returnFiber; do { switch (workInProgress.tag) { diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index ba0dfb5c32aa..b6ddcec76d39 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -41,7 +41,7 @@ import { enableLazyContextPropagation, enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; -import {createCapturedValue} from './ReactCapturedValue'; +import {createCapturedValueAtFiber} from './ReactCapturedValue'; import { enqueueCapturedUpdate, createUpdate, @@ -517,7 +517,7 @@ function throwException( // Even though the user may not be affected by this error, we should // still log it so it can be fixed. - queueHydrationError(value); + queueHydrationError(createCapturedValueAtFiber(value, sourceFiber)); return; } } else { @@ -525,12 +525,12 @@ function throwException( } } + value = createCapturedValueAtFiber(value, sourceFiber); + renderDidError(value); + // We didn't find a boundary that could handle this type of exception. Start // over and traverse parent path again, this time treating the exception // as an error. - renderDidError(value); - - value = createCapturedValue(value, sourceFiber); let workInProgress = returnFiber; do { switch (workInProgress.tag) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 9857207dfb23..82963ea0fa9d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -187,7 +187,10 @@ import { ContextOnlyDispatcher, getIsUpdatingOpaqueValueInRenderPhaseInDEV, } from './ReactFiberHooks.new'; -import {createCapturedValue} from './ReactCapturedValue'; +import { + createCapturedValueAtFiber, + type CapturedValue, +} from './ReactCapturedValue'; import { push as pushToStack, pop as popFromStack, @@ -310,10 +313,14 @@ let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes; // Lanes that were pinged (in an interleaved event) during this render. let workInProgressRootPingedLanes: Lanes = NoLanes; // Errors that are thrown during the render phase. -let workInProgressRootConcurrentErrors: Array | null = null; +let workInProgressRootConcurrentErrors: Array< + CapturedValue, +> | null = null; // These are errors that we recovered from without surfacing them to the UI. // We will log them once the tree commits. -let workInProgressRootRecoverableErrors: Array | null = null; +let workInProgressRootRecoverableErrors: Array< + CapturedValue, +> | null = null; // The most recent time we committed a fallback. This lets us ensure a train // model where we don't commit new loading states in too quick succession. @@ -998,7 +1005,7 @@ function recoverFromConcurrentError(root, errorRetryLanes) { return exitStatus; } -export function queueRecoverableErrors(errors: Array) { +export function queueRecoverableErrors(errors: Array>) { if (workInProgressRootRecoverableErrors === null) { workInProgressRootRecoverableErrors = errors; } else { @@ -1629,7 +1636,7 @@ export function renderDidSuspendDelayIfPossible(): void { } } -export function renderDidError(error: mixed) { +export function renderDidError(error: CapturedValue) { if (workInProgressRootExitStatus !== RootSuspendedWithDelay) { workInProgressRootExitStatus = RootErrored; } @@ -1950,7 +1957,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void { function commitRoot( root: FiberRoot, - recoverableErrors: null | Array, + recoverableErrors: null | Array>, transitions: Array | null, ) { // TODO: This no longer makes any sense. We already wrap the mutation and @@ -1977,7 +1984,7 @@ function commitRoot( function commitRootImpl( root: FiberRoot, - recoverableErrors: null | Array, + recoverableErrors: null | Array>, transitions: Array | null, renderPriorityLevel: EventPriority, ) { @@ -2274,7 +2281,9 @@ function commitRootImpl( const onRecoverableError = root.onRecoverableError; for (let i = 0; i < recoverableErrors.length; i++) { const recoverableError = recoverableErrors[i]; - onRecoverableError(recoverableError); + const componentStack = recoverableError.stack; + const digest = recoverableError.digest; + onRecoverableError(recoverableError.value, {componentStack, digest}); } } @@ -2554,7 +2563,7 @@ function captureCommitPhaseErrorOnRoot( sourceFiber: Fiber, error: mixed, ) { - const errorInfo = createCapturedValue(error, sourceFiber); + const errorInfo = createCapturedValueAtFiber(error, sourceFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane)); const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); @@ -2599,7 +2608,7 @@ export function captureCommitPhaseError( (typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance)) ) { - const errorInfo = createCapturedValue(error, sourceFiber); + const errorInfo = createCapturedValueAtFiber(error, sourceFiber); const update = createClassErrorUpdate( fiber, errorInfo, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 5a9f93bbada2..dc517877393c 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -187,7 +187,10 @@ import { ContextOnlyDispatcher, getIsUpdatingOpaqueValueInRenderPhaseInDEV, } from './ReactFiberHooks.old'; -import {createCapturedValue} from './ReactCapturedValue'; +import { + createCapturedValueAtFiber, + type CapturedValue, +} from './ReactCapturedValue'; import { push as pushToStack, pop as popFromStack, @@ -310,10 +313,14 @@ let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes; // Lanes that were pinged (in an interleaved event) during this render. let workInProgressRootPingedLanes: Lanes = NoLanes; // Errors that are thrown during the render phase. -let workInProgressRootConcurrentErrors: Array | null = null; +let workInProgressRootConcurrentErrors: Array< + CapturedValue, +> | null = null; // These are errors that we recovered from without surfacing them to the UI. // We will log them once the tree commits. -let workInProgressRootRecoverableErrors: Array | null = null; +let workInProgressRootRecoverableErrors: Array< + CapturedValue, +> | null = null; // The most recent time we committed a fallback. This lets us ensure a train // model where we don't commit new loading states in too quick succession. @@ -998,7 +1005,7 @@ function recoverFromConcurrentError(root, errorRetryLanes) { return exitStatus; } -export function queueRecoverableErrors(errors: Array) { +export function queueRecoverableErrors(errors: Array>) { if (workInProgressRootRecoverableErrors === null) { workInProgressRootRecoverableErrors = errors; } else { @@ -1629,7 +1636,7 @@ export function renderDidSuspendDelayIfPossible(): void { } } -export function renderDidError(error: mixed) { +export function renderDidError(error: CapturedValue) { if (workInProgressRootExitStatus !== RootSuspendedWithDelay) { workInProgressRootExitStatus = RootErrored; } @@ -1950,7 +1957,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void { function commitRoot( root: FiberRoot, - recoverableErrors: null | Array, + recoverableErrors: null | Array>, transitions: Array | null, ) { // TODO: This no longer makes any sense. We already wrap the mutation and @@ -1977,7 +1984,7 @@ function commitRoot( function commitRootImpl( root: FiberRoot, - recoverableErrors: null | Array, + recoverableErrors: null | Array>, transitions: Array | null, renderPriorityLevel: EventPriority, ) { @@ -2268,7 +2275,9 @@ function commitRootImpl( const onRecoverableError = root.onRecoverableError; for (let i = 0; i < recoverableErrors.length; i++) { const recoverableError = recoverableErrors[i]; - onRecoverableError(recoverableError); + const componentStack = recoverableError.stack; + const digest = recoverableError.digest; + onRecoverableError(recoverableError.value, {componentStack, digest}); } } @@ -2548,7 +2557,7 @@ function captureCommitPhaseErrorOnRoot( sourceFiber: Fiber, error: mixed, ) { - const errorInfo = createCapturedValue(error, sourceFiber); + const errorInfo = createCapturedValueAtFiber(error, sourceFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane)); const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); @@ -2593,7 +2602,7 @@ export function captureCommitPhaseError( (typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance)) ) { - const errorInfo = createCapturedValue(error, sourceFiber); + const errorInfo = createCapturedValueAtFiber(error, sourceFiber); const update = createClassErrorUpdate( fiber, errorInfo, diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 319bbc1c337d..618260b47c20 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -247,7 +247,10 @@ type BaseFiberRootProperties = {| // a reference to. identifierPrefix: string, - onRecoverableError: (error: mixed) => void, + onRecoverableError: ( + error: mixed, + errorInfo: {digest?: ?string, componentStack?: ?string}, + ) => void, |}; // The following attributes are only used by DevTools and are only present in DEV builds. diff --git a/packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js b/packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js index 22ae1e6d71ec..857a374f3e81 100644 --- a/packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js +++ b/packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js @@ -192,7 +192,7 @@ describe('ReactDOMServerFB', () => { expect(remaining).toEqual(''); expect(errors).toEqual([ - 'This Suspense boundary was aborted by the server', + 'This Suspense boundary was aborted by the server.', ]); }); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 5376ca2764ff..2b0a3d82e060 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -131,7 +131,7 @@ type LegacyContext = { type SuspenseBoundary = { id: SuspenseBoundaryID, rootSegmentID: number, - errorHash: ?string, // the error hash if it errors + 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 forceClientRender: boolean, // if it errors or infinitely suspends @@ -323,7 +323,7 @@ function createSuspenseBoundary( completedSegments: [], byteSize: 0, fallbackAbortableTasks, - errorHash: null, + errorDigest: null, }; } @@ -463,14 +463,14 @@ function captureBoundaryErrorDetailsDev( function logRecoverableError(request: Request, error: any): ?string { // If this callback errors, we intentionally let that error bubble up to become a fatal error // so that someone fixes the error reporting instead of hiding it. - const errorHash = request.onError(error); - if (errorHash != null && typeof errorHash !== 'string') { + const errorDigest = request.onError(error); + if (errorDigest != null && typeof errorDigest !== 'string') { // eslint-disable-next-line react-internal/prod-error-codes throw new Error( - `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`, + `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`, ); } - return errorHash; + return errorDigest; } function fatalError(request: Request, error: mixed): void { @@ -568,7 +568,7 @@ function renderSuspenseBoundary( } catch (error) { contentRootSegment.status = ERRORED; newBoundary.forceClientRender = true; - newBoundary.errorHash = logRecoverableError(request, error); + newBoundary.errorDigest = logRecoverableError(request, error); if (__DEV__) { captureBoundaryErrorDetailsDev(newBoundary, error); } @@ -1488,14 +1488,14 @@ function erroredTask( error: mixed, ) { // Report the error to a global handler. - const errorHash = logRecoverableError(request, error); + const errorDigest = logRecoverableError(request, error); if (boundary === null) { fatalError(request, error); } else { boundary.pendingTasks--; if (!boundary.forceClientRender) { boundary.forceClientRender = true; - boundary.errorHash = errorHash; + boundary.errorDigest = errorDigest; if (__DEV__) { captureBoundaryErrorDetailsDev(boundary, error); } @@ -1554,9 +1554,9 @@ function abortTask(task: Task): void { if (!boundary.forceClientRender) { boundary.forceClientRender = true; const error = new Error( - 'This Suspense boundary was aborted by the server', + 'This Suspense boundary was aborted by the server.', ); - boundary.errorHash = request.onError(error); + boundary.errorDigest = request.onError(error); if (__DEV__) { captureBoundaryErrorDetailsDev(boundary, error); } @@ -1838,7 +1838,7 @@ function flushSegment( writeStartClientRenderedSuspenseBoundary( destination, request.responseState, - boundary.errorHash, + boundary.errorDigest, boundary.errorMessage, boundary.errorComponentStack, ); @@ -1921,7 +1921,7 @@ function flushClientRenderedBoundary( destination, request.responseState, boundary.id, - boundary.errorHash, + boundary.errorDigest, boundary.errorMessage, boundary.errorComponentStack, ); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index a8c8810bbe49..826fe3b5db87 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -417,6 +417,6 @@ "429": "ServerContext: %s already defined", "430": "ServerContext can only have a value prop and children. Found: %s", "431": "React elements are not allowed in ServerContext", - "432": "This Suspense boundary was aborted by the server", + "432": "This Suspense boundary was aborted by the server.", "433": "useId can only be used while React is rendering" }