Skip to content

Commit

Permalink
Log a recoverable error whenever hydration fails (facebook#23319)
Browse files Browse the repository at this point in the history
There are several cases where hydration fails, server-rendered HTML is
discarded, and we fall back to client rendering. Whenever this happens,
we will now log an error with onRecoverableError, with a message
explaining why.

In some of these scenarios, this is not the only recoverable error that
is logged. For example, an error during hydration will cause hydration
to fail, which is itself an error. So we end up logging two separate
errors: the original error, and one that explains why hydration failed.

I've made sure that the original error always gets logged first, to
preserve the causal sequence.

Another thing we could do is aggregate the errors with the Error "cause"
feature and AggregateError. Since these are new-ish features in
JavaScript, we'd need a fallback behavior. I'll leave this for a
follow up.
  • Loading branch information
acdlite authored and zhengjitf committed Apr 15, 2022
1 parent 265588b commit 49857bd
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 41 deletions.
84 changes: 69 additions & 15 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,11 @@ describe('ReactDOMFizzServer', () => {
window.__INIT__ = function() {
bootstrapped = true;
// Attempt to hydrate the content.
ReactDOM.hydrateRoot(container, <App isClient={true} />);
ReactDOM.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
};

await act(async () => {
Expand Down Expand Up @@ -394,7 +398,10 @@ describe('ReactDOMFizzServer', () => {
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);

// Now we can client render it instead.
Scheduler.unstable_flushAll();
expect(Scheduler).toFlushAndYield([
'The server could not finish this Suspense boundary, likely due to ' +
'an error during server rendering. Switched to client rendering.',
]);

// The client rendered HTML is now in place.
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
Expand Down Expand Up @@ -465,7 +472,11 @@ describe('ReactDOMFizzServer', () => {
expect(loggedErrors).toEqual([]);

// Attempt to hydrate the content.
ReactDOM.hydrateRoot(container, <App isClient={true} />);
ReactDOM.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
Scheduler.unstable_flushAll();

// We're still loading because we're waiting for the server to stream more content.
Expand All @@ -484,7 +495,10 @@ describe('ReactDOMFizzServer', () => {
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);

// Now we can client render it instead.
Scheduler.unstable_flushAll();
expect(Scheduler).toFlushAndYield([
'The server could not finish this Suspense boundary, likely due to ' +
'an error during server rendering. Switched to client rendering.',
]);

// The client rendered HTML is now in place.
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
Expand Down Expand Up @@ -766,7 +780,11 @@ describe('ReactDOMFizzServer', () => {
// We're still showing a fallback.

// Attempt to hydrate the content.
ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
Scheduler.unstable_flushAll();

// We're still loading because we're waiting for the server to stream more content.
Expand All @@ -778,7 +796,10 @@ describe('ReactDOMFizzServer', () => {
});

// We still can't render it on the client.
Scheduler.unstable_flushAll();
expect(Scheduler).toFlushAndYield([
'The server could not finish this Suspense boundary, likely due to an ' +
'error during server rendering. Switched to client rendering.',
]);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);

// We now resolve it on the client.
Expand Down Expand Up @@ -1455,7 +1476,11 @@ describe('ReactDOMFizzServer', () => {
// We're still showing a fallback.

// Attempt to hydrate the content.
ReactDOM.hydrateRoot(container, <App isClient={true} />);
ReactDOM.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
Scheduler.unstable_flushAll();

// We're still loading because we're waiting for the server to stream more content.
Expand Down Expand Up @@ -1484,7 +1509,10 @@ describe('ReactDOMFizzServer', () => {
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);

// That will let us client render it instead.
Scheduler.unstable_flushAll();
expect(Scheduler).toFlushAndYield([
'The server could not finish this Suspense boundary, likely due to ' +
'an error during server rendering. Switched to client rendering.',
]);

// The client rendered HTML is now in place.
expect(getVisibleChildren(container)).toEqual(
Expand Down Expand Up @@ -1736,8 +1764,11 @@ describe('ReactDOMFizzServer', () => {
// The first paint switches to client rendering due to mismatch
expect(Scheduler).toFlushUntilNextPaint([
'client',
'Log recoverable error: An error occurred during hydration. ' +
'The server HTML was replaced with client content',
'Log recoverable error: Hydration failed because the initial ' +
'UI does not match what was rendered on the server.',
'Log recoverable error: There was an error while hydrating. ' +
'Because the error happened outside of a Suspense boundary, the ' +
'entire root will switch to client rendering.',
]);
}).toErrorDev(
[
Expand Down Expand Up @@ -1834,8 +1865,11 @@ describe('ReactDOMFizzServer', () => {
// The first paint switches to client rendering due to mismatch
expect(Scheduler).toFlushUntilNextPaint([
'client',
'Log recoverable error: An error occurred during hydration. ' +
'The server HTML was replaced with client content',
'Log recoverable error: Hydration failed because the initial ' +
'UI does not match what was rendered on the server.',
'Log recoverable error: There was an error while hydrating. ' +
'Because the error happened outside of a Suspense boundary, the ' +
'entire root will switch to client rendering.',
]);
}).toErrorDev(
[
Expand Down Expand Up @@ -1928,7 +1962,13 @@ describe('ReactDOMFizzServer', () => {
// An error logged but instead of surfacing it to the UI, we switched
// to client rendering.
expect(() => {
expect(Scheduler).toFlushAndYield(['Yay!', 'Hydration error']);
expect(Scheduler).toFlushAndYield([
'Yay!',
'Hydration error',
'There was an error while hydrating. Because the error happened ' +
'outside of a Suspense boundary, the entire root will switch ' +
'to client rendering.',
]);
}).toErrorDev(
'An error occurred during hydration. The server HTML was replaced',
{withoutStack: true},
Expand Down Expand Up @@ -2012,7 +2052,11 @@ describe('ReactDOMFizzServer', () => {

// An error logged but instead of surfacing it to the UI, we switched
// to client rendering.
expect(Scheduler).toFlushAndYield(['Yay!', 'Hydration error']);
expect(Scheduler).toFlushAndYield([
'Yay!',
'Hydration error',
'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
]);
expect(getVisibleChildren(container)).toEqual(
<div>
<span />
Expand Down Expand Up @@ -2178,7 +2222,11 @@ describe('ReactDOMFizzServer', () => {

// An error logged but instead of surfacing it to the UI, we switched
// to client rendering.
expect(Scheduler).toFlushAndYield(['Hydration error']);
expect(Scheduler).toFlushAndYield([
'Hydration error',
'There was an error while hydrating this Suspense boundary. Switched ' +
'to client rendering.',
]);
expect(getVisibleChildren(container)).toEqual(
<div>
<span />
Expand Down Expand Up @@ -2328,8 +2376,14 @@ describe('ReactDOMFizzServer', () => {
expect(Scheduler).toFlushAndYield([
'A',
'B',

'Logged recoverable error: Hydration error',
'Logged recoverable error: There was an error while hydrating this ' +
'Suspense boundary. Switched to client rendering.',

'Logged recoverable error: Hydration error',
'Logged recoverable error: There was an error while hydrating this ' +
'Suspense boundary. Switched to client rendering.',
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,23 @@ describe('ReactDOMFizzShellHydration', () => {

// Hydration suspends because the data for the shell hasn't loaded yet
const root = await clientAct(async () => {
return ReactDOM.hydrateRoot(container, <App />);
return ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
});
expect(Scheduler).toHaveYielded(['Suspend! [Shell]']);
expect(container.textContent).toBe('Shell');

await clientAct(async () => {
root.render(<Text text="New screen" />);
});
expect(Scheduler).toHaveYielded(['New screen']);
expect(Scheduler).toHaveYielded([
'This root received an early update, before anything was able ' +
'hydrate. Switched the entire root to client rendering.',
'New screen',
]);
expect(container.textContent).toBe('New screen');
});
});
Loading

0 comments on commit 49857bd

Please sign in to comment.