From 4f29ba1cc52061e439cede3813e100557b23a15c Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 6 Jun 2022 17:23:32 -0400 Subject: [PATCH] support errorInfo in onRecoverableError (#24591) * extend onRecoverableError API to support errorInfo errorInfo has been used in Error Boundaries wiht componentDidCatch for a while now. To date this metadata only contained a componentStack. onRecoverableError only receives an error (type mixed) argument and thus providing additional error metadata was not possible without mutating user created mixed objects. This change modifies rootConcurrentErrors rootRecoverableErrors, and hydrationErrors so all expect CapturedValue types. additionally a new factory function allows the creation of CapturedValues from a value plus a hash and stack. In general, client derived CapturedValues will be created using the original function which derives a componentStack from a fiber and server originated CapturedValues will be created using with a passed in hash and optional componentStack. --- .../src/__tests__/ReactDOMFizzServer-test.js | 191 +++++++++++------- .../ReactDOMFizzServerBrowser-test.js | 4 +- .../__tests__/ReactDOMFizzServerNode-test.js | 10 +- .../__tests__/ReactDOMHydrationDiff-test.js | 52 ++++- ...DOMServerPartialHydration-test.internal.js | 73 +++++-- .../src/client/ReactDOMHostConfig.js | 55 +++-- .../src/server/ReactDOMServerFormatConfig.js | 78 ++++--- .../ReactDOMServerLegacyFormatConfig.js | 9 +- .../server/ReactNativeServerFormatConfig.js | 4 +- .../src/ReactCapturedValue.js | 17 +- .../src/ReactFiberBeginWork.new.js | 73 ++++--- .../src/ReactFiberBeginWork.old.js | 73 ++++--- .../src/ReactFiberHydrationContext.new.js | 5 +- .../src/ReactFiberHydrationContext.old.js | 5 +- .../src/ReactFiberThrow.new.js | 10 +- .../src/ReactFiberThrow.old.js | 10 +- .../src/ReactFiberWorkLoop.new.js | 29 ++- .../src/ReactFiberWorkLoop.old.js | 29 ++- .../src/ReactInternalTypes.js | 5 +- .../ReactDOMServerFB-test.internal.js | 2 +- packages/react-server/src/ReactFizzServer.js | 26 +-- scripts/error-codes/codes.json | 2 +- 22 files changed, 495 insertions(+), 267 deletions(-) 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" }