diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 66ae2c25e6901..7104350fde00e 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 3aacf68cc3b26..a1429b0d2a173 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 cb5ec892bd2c8..a625a8df0e2f0 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 f6b5a0b428a7d..f2ddab69cdd0e 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 ? '' : '';
+ // @TODO changes made to sending Fizz errors to client led to the insertion of templates in client rendered
+ // suspense boundaries. This leaks in this test becuase the client rendered suspense boundary appears like
+ // unhydrated tail nodes and this template is the first match. When we add special case handling for client
+ // rendered suspense boundaries this test will likely change again
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
Array [
- "Warning: Did not expect server HTML to contain a in
.
+ "Warning: Did not expect server HTML to contain a ${mismatchEl} in
.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in
.",
@@ -809,11 +819,21 @@ describe('ReactDOMServerHydration', () => {
);
}
- expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
- Array [
- "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
- ]
- `);
+ // We gate this assertion becuase fb-classic uses PartialRenderer for renderToString and it does not
+ // serialize server errors and send to client
+ if (usingPartialRenderer) {
+ expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
+ Array [
+ "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
+ ]
+ `);
+ } else {
+ expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
+ Array [
+ "Caught [This Suspense boundary was aborted by the server.]",
+ ]
+ `);
+ }
});
// @gate __DEV__
@@ -834,11 +854,21 @@ describe('ReactDOMServerHydration', () => {
);
}
- expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
- Array [
- "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
- ]
- `);
+ // We gate this assertion becuase fb-classic uses PartialRenderer for renderToString and it does not
+ // serialize server errors and send to client
+ if (usingPartialRenderer) {
+ expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
+ Array [
+ "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
+ ]
+ `);
+ } else {
+ expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
+ Array [
+ "Caught [This Suspense boundary was aborted by the server.]",
+ ]
+ `);
+ }
});
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index e4d33645820e9..c27eabe3e97dc 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -19,6 +19,7 @@ let Suspense;
let SuspenseList;
let act;
let IdleEventPriority;
+let usingPartialRenderer;
function normalizeCodeLocInfo(strOrErr) {
if (strOrErr && strOrErr.replace) {
@@ -110,6 +111,8 @@ describe('ReactDOMServerPartialHydration', () => {
SuspenseList = React.SuspenseList;
}
+ usingPartialRenderer = global.__WWW__ && !__EXPERIMENTAL__;
+
IdleEventPriority = require('react-reconciler/constants').IdleEventPriority;
});
@@ -1668,10 +1671,16 @@ describe('ReactDOMServerPartialHydration', () => {
Scheduler.unstable_yieldValue(error.message);
},
});
- expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to ' +
- 'an error during server rendering. Switched to client rendering.',
- ]);
+ // we exclude fb bundles with partial renderer
+ if (__DEV__ && !usingPartialRenderer) {
+ expect(Scheduler).toFlushAndYield([
+ 'This Suspense boundary was aborted by the server.',
+ ]);
+ } else {
+ expect(Scheduler).toFlushAndYield([
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ ]);
+ }
jest.runAllTimers();
expect(container.textContent).toBe('Hello');
@@ -1730,10 +1739,16 @@ describe('ReactDOMServerPartialHydration', () => {
Scheduler.unstable_yieldValue(error.message);
},
});
- expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to ' +
- 'an error during server rendering. Switched to client rendering.',
- ]);
+ // we exclude fb bundles with partial renderer
+ if (__DEV__ && !usingPartialRenderer) {
+ expect(Scheduler).toFlushAndYield([
+ 'This Suspense boundary was aborted by the server.',
+ ]);
+ } else {
+ expect(Scheduler).toFlushAndYield([
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ ]);
+ }
// This will have exceeded the suspended time so we should timeout.
jest.advanceTimersByTime(500);
// The boundary should longer be suspended for the middle content
@@ -1797,10 +1812,16 @@ describe('ReactDOMServerPartialHydration', () => {
Scheduler.unstable_yieldValue(error.message);
},
});
- expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to ' +
- 'an error during server rendering. Switched to client rendering.',
- ]);
+ // we exclude fb bundles with partial renderer
+ if (__DEV__ && !usingPartialRenderer) {
+ expect(Scheduler).toFlushAndYield([
+ 'This Suspense boundary was aborted by the server.',
+ ]);
+ } else {
+ expect(Scheduler).toFlushAndYield([
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ ]);
+ }
// This will have exceeded the suspended time so we should timeout.
jest.advanceTimersByTime(500);
// The boundary should longer be suspended for the middle content
@@ -2115,10 +2136,16 @@ describe('ReactDOMServerPartialHydration', () => {
});
suspend = true;
- expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to ' +
- 'an error during server rendering. Switched to client rendering.',
- ]);
+ // we exclude fb bundles with partial renderer
+ if (__DEV__ && !usingPartialRenderer) {
+ expect(Scheduler).toFlushAndYield([
+ 'This Suspense boundary was aborted by the server.',
+ ]);
+ } else {
+ expect(Scheduler).toFlushAndYield([
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ ]);
+ }
// We haven't hydrated the second child but the placeholder is still in the list.
expect(container.textContent).toBe('ALoading B');
@@ -2178,10 +2205,16 @@ describe('ReactDOMServerPartialHydration', () => {
Scheduler.unstable_yieldValue(error.message);
},
});
- expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to ' +
- 'an error during server rendering. Switched to client rendering.',
- ]);
+ // we exclude fb bundles with partial renderer
+ if (__DEV__ && !usingPartialRenderer) {
+ expect(Scheduler).toFlushAndYield([
+ 'This Suspense boundary was aborted by the server.',
+ ]);
+ } else {
+ expect(Scheduler).toFlushAndYield([
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ ]);
+ }
jest.runAllTimers();
expect(ref.current).toBe(span);
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index fb7d4e66e91f3..17684548d4dba 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -729,29 +729,44 @@ export function isSuspenseInstancePending(instance: SuspenseInstance) {
export function isSuspenseInstanceFallback(instance: SuspenseInstance) {
return instance.data === SUSPENSE_FALLBACK_START_DATA;
}
+
export function getSuspenseInstanceFallbackErrorDetails(
instance: SuspenseInstance,
-) {
- const nextSibling = instance.nextSibling;
- let errorMessage /*, errorComponentStack, errorHash*/;
- if (
- nextSibling &&
- nextSibling.nodeType === ELEMENT_NODE &&
- nextSibling.nodeName.toLowerCase() === 'template'
- ) {
- const msg = ((nextSibling: any): HTMLTemplateElement).dataset.msg;
- if (msg !== null) errorMessage = msg;
-
- // @TODO read and return hash and componentStack once we know how we are goign to
- // expose this extra errorInfo to onRecoverableError
-
- // const hash = ((nextSibling: any): HTMLTemplateElement).dataset.hash;
- // if (hash !== null) errorHash = hash;
-
- // const stack = ((nextSibling: any): HTMLTemplateElement).dataset.stack;
- // if (stack !== null) errorComponentStack = stack;
+): {digest: ?string, message?: string, stack?: string} {
+ const dataset =
+ instance.nextSibling && ((instance.nextSibling: any): HTMLElement).dataset;
+ let digest, message, stack;
+ if (dataset) {
+ digest = dataset.dgst;
+ if (__DEV__) {
+ message = dataset.msg;
+ stack = dataset.stck;
+ }
+ }
+ if (__DEV__) {
+ return {
+ message,
+ digest,
+ stack,
+ };
+ } else {
+ // Object gets DCE'd if constructed in tail position and matches callsite destructuring
+ return {
+ digest,
+ };
}
- return {errorMessage /*, errorComponentStack, errorHash*/};
+
+ // let value = {message: undefined, hash: undefined};
+ // const nextSibling = instance.nextSibling;
+ // if (nextSibling) {
+ // const dataset = ((nextSibling: any): HTMLTemplateElement).dataset;
+ // value.message = dataset.msg;
+ // value.hash = dataset.hash;
+ // if (__DEV__) {
+ // value.stack = dataset.stack;
+ // }
+ // }
+ // return value;
}
export function registerSuspenseInstanceRetry(
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index f006477c4b0a3..95e02a41b4632 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -1527,16 +1527,22 @@ const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(
const endSuspenseBoundary = stringToPrecomputedChunk('');
const clientRenderedSuspenseBoundaryError1 = stringToPrecomputedChunk(
- '
',
+ '>',
);
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 71716d7b71f28..375562e80b56d 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 d65a30180a4db..3c2c23c911faf 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 8b87eb9a6c1b3..64b1f18515d5a 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 4f227ed4710c4..aa7ec7c5f9bdf 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 63a8ae2212c7c..4601d0dded1c0 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 636a467475df4..fc18efab37169 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 3befb348c05ab..099b02fbcecc3 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 3d13cd6407b43..f3dc2edf00f01 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 ba0dfb5c32aa7..b6ddcec76d39f 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 9857207dfb23e..82963ea0fa9d1 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 5a9f93bbada24..dc517877393c9 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 319bbc1c337dd..618260b47c209 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 22ae1e6d71ec2..857a374f3e81a 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 5376ca2764ff2..2b0a3d82e0600 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 a8c8810bbe49e..826fe3b5db870 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"
}