Skip to content

Commit

Permalink
extend onRecoverableError API to support errorInfo
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnoff committed May 30, 2022
1 parent aec5759 commit c17a9ce
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 168 deletions.
191 changes: 119 additions & 72 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Expand Up @@ -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 errorHash = errorInfo && errorInfo.errorHash;
if (stack) {
return [error.message, errorHash, normalizeCodeLocInfo(stack)];
} else if (errorHash) {
return [error.message, errorHash];
}
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();
Expand Down Expand Up @@ -471,8 +453,8 @@ describe('ReactDOMFizzServer', () => {
bootstrapped = true;
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
};
Expand All @@ -483,8 +465,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return 'Hash of (' + x.message + ')';
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedHash = onError(theError);
loggedErrors.length = 0;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
Expand Down Expand Up @@ -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,
expectedHash,
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.',
expectedHash,
],
],
);

Expand Down Expand Up @@ -577,8 +568,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return 'hash of (' + x.message + ')';
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedHash = onError(theError);
loggedErrors.length = 0;

function App({isClient}) {
return (
Expand All @@ -605,8 +596,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand All @@ -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,
expectedHash,
componentStack(['Suspense', 'div', 'App']),
],
],
[
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
expectedHash,
],
],
);

Expand Down Expand Up @@ -675,8 +675,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return 'hash(' + x.message + ')';
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedHash = onError(theError);
loggedErrors.length = 0;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
Expand All @@ -693,8 +693,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand All @@ -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,
expectedHash,
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.',
expectedHash,
],
],
);
});
Expand Down Expand Up @@ -735,8 +744,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return 'hash(' + x.message + ')';
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedHash = onError(theError);
loggedErrors.length = 0;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
Expand All @@ -753,8 +762,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand All @@ -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,
expectedHash,
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.',
expectedHash,
],
],
);

Expand Down Expand Up @@ -1053,9 +1071,10 @@ describe('ReactDOMFizzServer', () => {
}

const loggedErrors = [];
const expectedHash = 'Hash for Abort';
function onError(error) {
loggedErrors.push(error);
return `Hash of (${error.message})`;
return expectedHash;
}

let controls;
Expand All @@ -1069,8 +1088,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand All @@ -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', expectedHash]],
[
'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.',
expectedHash,
],
],
);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
Expand Down Expand Up @@ -1755,8 +1777,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return `hash of (${x.message})`;
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedHash = onError(theError);
loggedErrors.length = 0;

let controls;
await act(async () => {
Expand All @@ -1775,8 +1797,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand Down Expand Up @@ -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,
expectedHash,
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.',
expectedHash,
],
],
);

Expand Down Expand Up @@ -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 expectedHash = onError(theError);
loggedErrors.length = 0;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
Expand All @@ -3156,18 +3194,27 @@ describe('ReactDOMFizzServer', () => {

const errors = [];
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
expect(Scheduler).toFlushAndYield([]);

// 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,
expectedHash,
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.',
expectedHash,
],
],
);
});
Expand Down
21 changes: 7 additions & 14 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Expand Up @@ -731,27 +731,20 @@ export function isSuspenseInstanceFallback(instance: SuspenseInstance) {
}
export function getSuspenseInstanceFallbackErrorDetails(
instance: SuspenseInstance,
) {
): {message?: string, stack?: string, hash?: string} {
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;
return {
message: ((nextSibling: any): HTMLTemplateElement).dataset.msg,
stack: ((nextSibling: any): HTMLTemplateElement).dataset.stack,
hash: ((nextSibling: any): HTMLTemplateElement).dataset.hash,
};
}
return {errorMessage /*, errorComponentStack, errorHash*/};
return {};
}

export function registerSuspenseInstanceRetry(
Expand Down

0 comments on commit c17a9ce

Please sign in to comment.