@samitouri / QOS-React-1 / commits / 6090cab099

Use a Wrapper Error for onRecoverableError with a "cause" Field for the real Error (#28736)

We basically have four kinds of recoverable errors: - Hydration mismatches. - Server errored but client didn't. - Hydration render errored but client render didn't (in Root or Suspense boundary). - Concurrent render errored but synchronous render didn't. For the first three we log an additional error that the root or Suspense boundary didn't error. This provides some context about what happened. However, the problem is that for hydration mismatches that's unnecessary extra context that is confusing. We also don't log any additional context for concurrent render errors that could recover. This used to be the only recoverable error so it didn't need extra context but now we need to distinguish them. When we log these to `reportError` it's confusing to just see the error because you didn't see anything error on the page. It's also hard to group them together as one. In this PR, I remove the unnecessary context for hydration mismatches. For hydration and concurrent errors, I now wrap them in an error that describes that what happened but then use the new `cause` field to link the original error so we can keep that as the cause. The error that happened was that hydration client rendered or you deopted to sync render, the cause of that error is some other error. For server errors, we control the Error object so I already had added some context to that error object's message. Since we hide the message in prod, it's nice not to have the raw message in DEV neither. We could potentially split these into two errors for parity though.

Sebastian Markbåge committed Apr 3, 2024 at 21:53 UTC 6090cab099a8f7f373e04c7eb2937425a8f80f80
16 files changed +437 -286
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+97 -73
@@ -2417,17 +2417,17 @@ describe('ReactDOMFizzServer', () => {
2417
2418 ReactDOMClient.hydrateRoot(container, <App />, {
2419 onRecoverableError(error) {
2420 - Scheduler.log(
2421 - 'Log recoverable error: ' + normalizeError(error.message),
2422 - );
2420 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2421 + if (error.cause) {
2422 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2423 + }
2424 },
2425 });
2426
2427 // The first paint switches to client rendering due to mismatch
2428 await waitForPaint([
2429 'client',
2429 - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
2430 - 'Log recoverable error: There was an error while hydrating.',
2430 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
2431 ]);
2432 expect(getVisibleChildren(container)).toEqual(<div>client</div>);
2433 });
@@ -2489,9 +2489,7 @@ describe('ReactDOMFizzServer', () => {
2489
2490 ReactDOMClient.hydrateRoot(container, <App />, {
2491 onRecoverableError(error) {
2492 - Scheduler.log(
2493 - 'Log recoverable error: ' + normalizeError(error.message),
2494 - );
2492 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2493 },
2494 });
2495
@@ -2499,8 +2497,7 @@ describe('ReactDOMFizzServer', () => {
2497 // The first paint switches to client rendering due to mismatch
2498 await waitForPaint([
2499 'client',
2502 - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
2503 - 'Log recoverable error: There was an error while hydrating.',
2500 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
2501 ]);
2502 expect(getVisibleChildren(container)).toEqual(<div>client</div>);
2503 });
@@ -2561,7 +2558,10 @@ describe('ReactDOMFizzServer', () => {
2558 isClient = true;
2559 ReactDOMClient.hydrateRoot(container, <App />, {
2560 onRecoverableError(error) {
2564 - Scheduler.log(normalizeError(error.message));
2561 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2562 + if (error.cause) {
2563 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2564 + }
2565 },
2566 });
2567
@@ -2569,8 +2569,8 @@ describe('ReactDOMFizzServer', () => {
2569 // to client rendering.
2570 await waitForAll([
2571 'Yay!',
2572 - 'Hydration error',
2573 - 'There was an error while hydrating.',
2572 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering the entire root.',
2573 + 'Cause: Hydration error',
2574 ]);
2575 expect(getVisibleChildren(container)).toEqual(<span>Yay!</span>);
2576
@@ -2736,7 +2736,10 @@ describe('ReactDOMFizzServer', () => {
2736 isClient = true;
2737 ReactDOMClient.hydrateRoot(container, <App />, {
2738 onRecoverableError(error) {
2739 - Scheduler.log(normalizeError(error.message));
2739 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2740 + if (error.cause) {
2741 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2742 + }
2743 },
2744 });
2745
@@ -2744,8 +2747,8 @@ describe('ReactDOMFizzServer', () => {
2747 // to client rendering.
2748 await waitForAll([
2749 'Yay!',
2747 - 'Hydration error',
2748 - 'There was an error while hydrating this Suspense boundary.',
2750 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.',
2751 + 'Cause: Hydration error',
2752 ]);
2753 expect(getVisibleChildren(container)).toEqual(
2754 <div>
@@ -2884,7 +2887,10 @@ describe('ReactDOMFizzServer', () => {
2887 isClient = true;
2888 ReactDOMClient.hydrateRoot(container, <App />, {
2889 onRecoverableError(error) {
2887 - Scheduler.log('[c!] ' + error.message);
2890 + Scheduler.log('onRecoverableError: ' + error.message);
2891 + if (error.cause) {
2892 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2893 + }
2894 },
2895 });
2896 // This should not report any errors yet.
@@ -2908,7 +2914,7 @@ describe('ReactDOMFizzServer', () => {
2914 });
2915 await waitForAll([
2916 'Yay!',
2911 - '[c!] The server could not finish this Suspense boundary, ' +
2917 + 'onRecoverableError: The server could not finish this Suspense boundary, ' +
2918 'likely due to an error during server rendering. ' +
2919 'Switched to client rendering.',
2920 ]);
@@ -2969,7 +2975,10 @@ describe('ReactDOMFizzServer', () => {
2975 isClient = true;
2976 const root = ReactDOMClient.hydrateRoot(container, <App color="red" />, {
2977 onRecoverableError(error) {
2972 - Scheduler.log('[c!] ' + error.message);
2978 + Scheduler.log('onRecoverableError: ' + error.message);
2979 + if (error.cause) {
2980 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2981 + }
2982 },
2983 });
2984 // This should not report any errors yet.
@@ -3002,7 +3011,7 @@ describe('ReactDOMFizzServer', () => {
3011 });
3012 await waitForAll([
3013 'Yay! (red)',
3005 - '[c!] The server could not finish this Suspense boundary, ' +
3014 + 'onRecoverableError: The server could not finish this Suspense boundary, ' +
3015 'likely due to an error during server rendering. ' +
3016 'Switched to client rendering.',
3017 'Yay! (blue)',
@@ -3072,7 +3081,10 @@ describe('ReactDOMFizzServer', () => {
3081 <App fallbackText="Loading..." />,
3082 {
3083 onRecoverableError(error) {
3075 - Scheduler.log('[c!] ' + error.message);
3084 + Scheduler.log('onRecoverableError: ' + error.message);
3085 + if (error.cause) {
3086 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
3087 + }
3088 },
3089 },
3090 );
@@ -3097,7 +3109,7 @@ describe('ReactDOMFizzServer', () => {
3109 await waitForAll([]);
3110 jest.runAllTimers();
3111 assertLog([
3100 - '[c!] The server could not finish this Suspense boundary, ' +
3112 + 'onRecoverableError: The server could not finish this Suspense boundary, ' +
3113 'likely due to an error during server rendering. ' +
3114 'Switched to client rendering.',
3115 ]);
@@ -3191,15 +3203,18 @@ describe('ReactDOMFizzServer', () => {
3203 isClient = true;
3204 ReactDOMClient.hydrateRoot(container, <App />, {
3205 onRecoverableError(error) {
3194 - Scheduler.log(normalizeError(error.message));
3206 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
3207 + if (error.cause) {
3208 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
3209 + }
3210 },
3211 });
3212
3213 // An error logged but instead of surfacing it to the UI, we switched
3214 // to client rendering.
3215 await waitForAll([
3201 - 'Hydration error',
3202 - 'There was an error while hydrating this Suspense boundary.',
3216 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.',
3217 + 'Cause: Hydration error',
3218 ]);
3219 expect(getVisibleChildren(container)).toEqual(
3220 <div>
@@ -3259,9 +3274,10 @@ describe('ReactDOMFizzServer', () => {
3274
3275 const root = ReactDOMClient.createRoot(container, {
3276 onRecoverableError(error) {
3262 - Scheduler.log(
3263 - 'Logged a recoverable error: ' + normalizeError(error.message),
3264 - );
3277 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
3278 + if (error.cause) {
3279 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
3280 + }
3281 },
3282 });
3283 React.startTransition(() => {
@@ -3281,7 +3297,8 @@ describe('ReactDOMFizzServer', () => {
3297 'B',
3298
3299 // Log the error
3284 - 'Logged a recoverable error: Oops!',
3300 + 'onRecoverableError: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.',
3301 + 'Cause: Oops!',
3302 ]);
3303
3304 // UI looks normal
@@ -3337,9 +3354,10 @@ describe('ReactDOMFizzServer', () => {
3354 isClient = true;
3355 ReactDOMClient.hydrateRoot(container, <App />, {
3356 onRecoverableError(error) {
3340 - Scheduler.log(
3341 - 'Logged recoverable error: ' + normalizeError(error.message),
3342 - );
3357 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
3358 + if (error.cause) {
3359 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
3360 + }
3361 },
3362 });
3363
@@ -3347,13 +3365,11 @@ describe('ReactDOMFizzServer', () => {
3365 'A',
3366 'B',
3367
3350 - 'Logged recoverable error: Hydration error',
3351 - 'Logged recoverable error: There was an error while hydrating this ' +
3352 - 'Suspense boundary.',
3368 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.',
3369 + 'Cause: Hydration error',
3370
3354 - 'Logged recoverable error: Hydration error',
3355 - 'Logged recoverable error: There was an error while hydrating this ' +
3356 - 'Suspense boundary.',
3371 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.',
3372 + 'Cause: Hydration error',
3373 ]);
3374 });
3375
@@ -4399,9 +4415,10 @@ describe('ReactDOMFizzServer', () => {
4415 const [ClientApp, clientResolve] = makeApp();
4416 ReactDOMClient.hydrateRoot(container, <ClientApp />, {
4417 onRecoverableError(error) {
4402 - Scheduler.log(
4403 - 'Logged recoverable error: ' + normalizeError(error.message),
4404 - );
4418 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
4419 + if (error.cause) {
4420 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
4421 + }
4422 },
4423 });
4424 await waitForAll([]);
@@ -4478,9 +4495,10 @@ describe('ReactDOMFizzServer', () => {
4495 const [ClientApp, clientResolve] = makeApp();
4496 ReactDOMClient.hydrateRoot(container, <ClientApp text="replaced" />, {
4497 onRecoverableError(error) {
4481 - Scheduler.log(
4482 - 'Logged recoverable error: ' + normalizeError(error.message),
4483 - );
4498 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
4499 + if (error.cause) {
4500 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
4501 + }
4502 },
4503 });
4504 await waitForAll([]);
@@ -4496,8 +4514,7 @@ describe('ReactDOMFizzServer', () => {
4514 // client-side rendering.
4515 await clientResolve();
4516 await waitForAll([
4499 - "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
4500 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4517 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
4518 ]);
4519 expect(getVisibleChildren(container)).toEqual(
4520 <div>
@@ -4545,14 +4562,14 @@ describe('ReactDOMFizzServer', () => {
4562
4563 ReactDOMClient.hydrateRoot(container, <App text="replaced" />, {
4564 onRecoverableError(error) {
4548 - Scheduler.log(
4549 - 'Logged recoverable error: ' + normalizeError(error.message),
4550 - );
4565 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
4566 + if (error.cause) {
4567 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
4568 + }
4569 },
4570 });
4571 await waitForAll([
4554 - "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
4555 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4572 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
4573 ]);
4574
4575 expect(getVisibleChildren(container)).toEqual(
@@ -4620,14 +4637,15 @@ describe('ReactDOMFizzServer', () => {
4637
4638 ReactDOMClient.hydrateRoot(container, <App />, {
4639 onRecoverableError(error) {
4623 - Scheduler.log(
4624 - 'Logged recoverable error: ' + normalizeError(error.message),
4625 - );
4640 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
4641 + if (error.cause) {
4642 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
4643 + }
4644 },
4645 });
4646 await waitForAll([
4629 - 'Logged recoverable error: uh oh',
4630 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4647 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.',
4648 + 'Cause: uh oh',
4649 ]);
4650
4651 expect(getVisibleChildren(container)).toEqual(
@@ -4709,9 +4727,10 @@ describe('ReactDOMFizzServer', () => {
4727
4728 ReactDOMClient.hydrateRoot(container, <App />, {
4729 onRecoverableError(error) {
4712 - Scheduler.log(
4713 - 'Logged recoverable error: ' + normalizeError(error.message),
4714 - );
4730 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
4731 + if (error.cause) {
4732 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
4733 + }
4734 },
4735 });
4736 await waitForAll([
@@ -4719,8 +4738,8 @@ describe('ReactDOMFizzServer', () => {
4738
4739 // onRecoverableError because the UI recovered without surfacing the
4740 // error to the user.
4722 - 'Logged recoverable error: first error',
4723 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4741 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.',
4742 + 'Cause: first error',
4743 ]);
4744 expect(mockError.mock.calls).toEqual([]);
4745 mockError.mockClear();
@@ -4828,9 +4847,10 @@ describe('ReactDOMFizzServer', () => {
4847
4848 ReactDOMClient.hydrateRoot(container, <App />, {
4849 onRecoverableError(error) {
4831 - Scheduler.log(
4832 - 'Logged recoverable error: ' + normalizeError(error.message),
4833 - );
4850 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
4851 + if (error.cause) {
4852 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
4853 + }
4854 },
4855 });
4856 await waitForAll(['suspending']);
@@ -4846,8 +4866,8 @@ describe('ReactDOMFizzServer', () => {
4866 await unsuspend();
4867 await waitForAll([
4868 'throwing: first error',
4849 - 'Logged recoverable error: first error',
4850 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4869 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.',
4870 + 'Cause: first error',
4871 ]);
4872 expect(getVisibleChildren(container)).toEqual(
4873 <div>
@@ -4954,16 +4974,17 @@ describe('ReactDOMFizzServer', () => {
4974
4975 ReactDOMClient.hydrateRoot(container, <App />, {
4976 onRecoverableError(error) {
4957 - Scheduler.log(
4958 - 'Logged recoverable error: ' + normalizeError(error.message),
4959 - );
4977 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
4978 + if (error.cause) {
4979 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
4980 + }
4981 },
4982 });
4983 await waitForAll([
4984 'throwing: first error',
4985 'suspending',
4965 - 'Logged recoverable error: first error',
4966 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4986 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.',
4987 + 'Cause: first error',
4988 ]);
4989 expect(mockError.mock.calls).toEqual([]);
4990 mockError.mockClear();
@@ -5368,7 +5389,10 @@ describe('ReactDOMFizzServer', () => {
5389 const errors = [];
5390 ReactDOMClient.hydrateRoot(container, <App />, {
5391 onRecoverableError(error) {
5371 - errors.push(error.message);
5392 + errors.push('onRecoverableError: ' + normalizeError(error.message));
5393 + if (error.cause) {
5394 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
5395 + }
5396 },
5397 });
5398 await waitForAll([]);
@@ -6336,7 +6360,7 @@ describe('ReactDOMFizzServer', () => {
6360 },
6361 });
6362 await waitForAll([]);
6339 - expect(errors.length).toEqual(2);
6363 + expect(errors.length).toEqual(1);
6364 expect(getVisibleChildren(container)).toEqual(<span />);
6365 });
6366 });
packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
+11 -2
@@ -397,6 +397,9 @@ describe('ReactDOMFizzShellHydration', () => {
397 },
398 onRecoverableError(error) {
399 Scheduler.log('onRecoverableError: ' + error.message);
400 + if (error.cause) {
401 + Scheduler.log('Cause: ' + error.cause.message);
402 + }
403 },
404 });
405 });
@@ -462,6 +465,9 @@ describe('ReactDOMFizzShellHydration', () => {
465 },
466 onRecoverableError(error) {
467 Scheduler.log('onRecoverableError: ' + error.message);
468 + if (error.cause) {
469 + Scheduler.log('Cause: ' + error.cause.message);
470 + }
471 },
472 });
473 });
@@ -529,13 +535,16 @@ describe('ReactDOMFizzShellHydration', () => {
535 },
536 onRecoverableError(error) {
537 Scheduler.log('onRecoverableError: ' + error.message);
538 + if (error.cause) {
539 + Scheduler.log('Cause: ' + error.cause.message);
540 + }
541 },
542 });
543 });
544
545 assertLog([
537 - 'onRecoverableError: plain error',
538 - 'onRecoverableError: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
546 + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering the entire root.',
547 + 'Cause: plain error',
548 ]);
549 expect(container.textContent).toBe('Hello world');
550 });
packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js
+55 -26
@@ -165,7 +165,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
165 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
166 onRecoverableError(error) {
167 // Don't miss a hydration error. There should be none.
168 - Scheduler.log(normalizeError(error.message));
168 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
169 + if (error.cause) {
170 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
171 + }
172 },
173 });
174 await waitForAll([]);
@@ -205,7 +208,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
208 );
209 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
210 onRecoverableError(error) {
208 - Scheduler.log(normalizeError(error.message));
211 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
212 + if (error.cause) {
213 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
214 + }
215 },
216 });
217 await waitForAll([]);
@@ -246,12 +252,14 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
252 );
253 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
254 onRecoverableError(error) {
249 - Scheduler.log(normalizeError(error.message));
255 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
256 + if (error.cause) {
257 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
258 + }
259 },
260 });
261 await waitForAll([
253 - "Hydration failed because the server rendered HTML didn't match the client.",
254 - 'There was an error while hydrating.',
262 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
263 ]);
264 expect(getVisibleChildren(container)).toEqual(
265 <div>
@@ -283,7 +291,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
291 );
292 const root = ReactDOMClient.hydrateRoot(container, <App text="Client" />, {
293 onRecoverableError(error) {
286 - Scheduler.log(normalizeError(error.message));
294 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
295 + if (error.cause) {
296 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
297 + }
298 },
299 });
300 await waitForAll([]);
@@ -327,12 +338,14 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
338 );
339 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
340 onRecoverableError(error) {
330 - Scheduler.log(normalizeError(error.message));
341 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
342 + if (error.cause) {
343 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
344 + }
345 },
346 });
347 await waitForAll([
334 - "Hydration failed because the server rendered HTML didn't match the client.",
335 - 'There was an error while hydrating.',
348 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
349 ]);
350 expect(getVisibleChildren(container)).toEqual(
351 <div>
@@ -367,12 +380,14 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
380 );
381 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
382 onRecoverableError(error) {
370 - Scheduler.log(normalizeError(error.message));
383 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
384 + if (error.cause) {
385 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
386 + }
387 },
388 });
389 await waitForAll([
374 - "Hydration failed because the server rendered HTML didn't match the client.",
375 - 'There was an error while hydrating.',
390 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
391 ]);
392 expect(getVisibleChildren(container)).toEqual(
393 <div>
@@ -410,12 +425,14 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
425 );
426 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
427 onRecoverableError(error) {
413 - Scheduler.log(normalizeError(error.message));
428 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
429 + if (error.cause) {
430 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
431 + }
432 },
433 });
434 await waitForAll([
417 - "Hydration failed because the server rendered HTML didn't match the client.",
418 - 'There was an error while hydrating.',
435 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
436 ]);
437 expect(getVisibleChildren(container)).toEqual(
438 <div>
@@ -451,12 +468,14 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
468 );
469 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
470 onRecoverableError(error) {
454 - Scheduler.log(normalizeError(error.message));
471 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
472 + if (error.cause) {
473 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
474 + }
475 },
476 });
477 await waitForAll([
458 - "Hydration failed because the server rendered HTML didn't match the client.",
459 - 'There was an error while hydrating.',
478 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
479 ]);
480 expect(getVisibleChildren(container)).toEqual(
481 <div>
@@ -496,7 +515,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
515 );
516 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
517 onRecoverableError(error) {
499 - Scheduler.log(normalizeError(error.message));
518 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
519 + if (error.cause) {
520 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
521 + }
522 },
523 });
524 await waitForAll([]);
@@ -533,7 +555,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
555 );
556 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
557 onRecoverableError(error) {
536 - Scheduler.log(normalizeError(error.message));
558 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
559 + if (error.cause) {
560 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
561 + }
562 },
563 });
564 await waitForAll([]);
@@ -566,12 +591,14 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
591 );
592 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
593 onRecoverableError(error) {
569 - Scheduler.log(normalizeError(error.message));
594 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
595 + if (error.cause) {
596 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
597 + }
598 },
599 });
600 await waitForAll([
573 - "Hydration failed because the server rendered HTML didn't match the client.",
574 - 'There was an error while hydrating.',
601 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
602 ]);
603 expect(getVisibleChildren(container)).toEqual(
604 <div>
@@ -604,12 +631,14 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
631 );
632 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
633 onRecoverableError(error) {
607 - Scheduler.log(normalizeError(error.message));
634 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
635 + if (error.cause) {
636 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
637 + }
638 },
639 });
640 await waitForAll([
611 - "Hydration failed because the server rendered HTML didn't match the client.",
612 - 'There was an error while hydrating.',
641 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
642 ]);
643 expect(getVisibleChildren(container)).toEqual(
644 <div>
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+30 -49
@@ -55,6 +55,15 @@ describe('ReactDOMServerHydration', () => {
55 function formatMessage(args) {
56 const [format, ...rest] = args;
57 if (format instanceof Error) {
58 + if (format.cause instanceof Error) {
59 + return (
60 + 'Caught [' +
61 + format.message +
62 + ']\n Cause [' +
63 + format.cause.message +
64 + ']'
65 + );
66 + }
67 return 'Caught [' + format.message + ']';
68 }
69 rest[rest.length - 1] = normalizeCodeLocInfo(rest[rest.length - 1]);
@@ -88,28 +97,27 @@ describe('ReactDOMServerHydration', () => {
97 }
98 if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
99 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
91 - [
92 - "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
93 -
94 - - A server/client branch \`if (typeof window !== 'undefined')\`.
95 - - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
96 - - Date formatting in a user's locale which doesn't match the server.
97 - - External changing data without sending a snapshot of it along with the HTML.
98 - - Invalid HTML tag nesting.
99 -
100 - It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
101 -
102 - https://react.dev/link/hydration-mismatch
103 -
104 - <Mismatch isClient={true}>
105 - <div className="parent">
106 - <main className="child">
107 - + client
108 - - server
109 - ]",
110 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
111 - ]
112 - `);
100 + [
101 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
102 +
103 + - A server/client branch \`if (typeof window !== 'undefined')\`.
104 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
105 + - Date formatting in a user's locale which doesn't match the server.
106 + - External changing data without sending a snapshot of it along with the HTML.
107 + - Invalid HTML tag nesting.
108 +
109 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
110 +
111 + https://react.dev/link/hydration-mismatch
112 +
113 + <Mismatch isClient={true}>
114 + <div className="parent">
115 + <main className="child">
116 + + client
117 + - server
118 + ]",
119 + ]
120 + `);
121 } else {
122 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
123 [
@@ -170,7 +178,6 @@ describe('ReactDOMServerHydration', () => {
178 + This markup contains an nbsp entity:   client text
179 - This markup contains an nbsp entity:   server text
180 ]",
173 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
181 ]
182 `);
183 } else {
@@ -477,7 +484,6 @@ describe('ReactDOMServerHydration', () => {
484 <div className="parent">
485 + <main className="only">
486 ]",
480 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
487 ]
488 `);
489 });
@@ -513,7 +519,6 @@ describe('ReactDOMServerHydration', () => {
519 - <main className="2">
520 ...
521 ]",
516 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
522 ]
523 `);
524 });
@@ -550,7 +555,6 @@ describe('ReactDOMServerHydration', () => {
555 - <footer className="3">
556 ...
557 ]",
553 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
558 ]
559 `);
560 });
@@ -586,7 +590,6 @@ describe('ReactDOMServerHydration', () => {
590 <main>
591 + <footer className="3">
592 ]",
589 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
593 ]
594 `);
595 });
@@ -618,7 +621,6 @@ describe('ReactDOMServerHydration', () => {
621 + only
622 -
623 ]",
621 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
624 ]
625 `);
626 } else {
@@ -678,7 +680,6 @@ describe('ReactDOMServerHydration', () => {
680 - <footer className="3">
681 ...
682 ]",
681 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
683 ]
684 `);
685 });
@@ -714,7 +715,6 @@ describe('ReactDOMServerHydration', () => {
715 - <main className="2">
716 ...
717 ]",
717 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
718 ]
719 `);
720 });
@@ -750,7 +750,6 @@ describe('ReactDOMServerHydration', () => {
750 <main>
751 + third
752 ]",
753 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
753 ]
754 `);
755 });
@@ -786,7 +785,6 @@ describe('ReactDOMServerHydration', () => {
785 <div className="parent">
786 - <main className="only">
787 ]",
789 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
788 ]
789 `);
790 });
@@ -822,7 +820,6 @@ describe('ReactDOMServerHydration', () => {
820 - <header className="1">
821 ...
822 ]",
825 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
823 ]
824 `);
825 });
@@ -858,7 +855,6 @@ describe('ReactDOMServerHydration', () => {
855 + <footer className="3">
856 - <main className="2">
857 ]",
861 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
858 ]
859 `);
860 });
@@ -892,7 +888,6 @@ describe('ReactDOMServerHydration', () => {
888 <div className="parent">
889 - <footer className="3">
890 ]",
895 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
891 ]
892 `);
893 });
@@ -922,7 +917,6 @@ describe('ReactDOMServerHydration', () => {
917 <div className="parent">
918 - only
919 ]",
925 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
920 ]
921 `);
922 });
@@ -958,7 +952,6 @@ describe('ReactDOMServerHydration', () => {
952 - first
953 ...
954 ]",
961 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
955 ]
956 `);
957 });
@@ -994,7 +987,6 @@ describe('ReactDOMServerHydration', () => {
987 + <footer className="3">
988 - second
989 ]",
997 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
990 ]
991 `);
992 });
@@ -1028,7 +1020,6 @@ describe('ReactDOMServerHydration', () => {
1020 <div className="parent">
1021 - third
1022 ]",
1031 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1023 ]
1024 `);
1025 });
@@ -1072,7 +1063,6 @@ describe('ReactDOMServerHydration', () => {
1063 <div className="parent">
1064 + <Suspense fallback={<p>}>
1065 ]",
1075 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1066 ]
1067 `);
1068 });
@@ -1108,7 +1098,6 @@ describe('ReactDOMServerHydration', () => {
1098 <div className="parent">
1099 - <Suspense>
1100 ]",
1111 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1101 ]
1102 `);
1103 });
@@ -1146,7 +1135,6 @@ describe('ReactDOMServerHydration', () => {
1135 <div className="parent">
1136 + <Suspense fallback={<p>}>
1137 ]",
1149 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1138 ]
1139 `);
1140 });
@@ -1188,7 +1176,6 @@ describe('ReactDOMServerHydration', () => {
1176 <div className="parent">
1177 - <Suspense>
1178 ]",
1191 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1179 ]
1180 `);
1181 });
@@ -1228,7 +1215,6 @@ describe('ReactDOMServerHydration', () => {
1215 - <footer className="3">
1216 ...
1217 ]",
1231 - "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
1218 ]
1219 `);
1220 });
@@ -1267,7 +1253,6 @@ describe('ReactDOMServerHydration', () => {
1253 + <footer className="3">
1254 - <main className="second">
1255 ]",
1270 - "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
1256 ]
1257 `);
1258 });
@@ -1364,7 +1349,6 @@ describe('ReactDOMServerHydration', () => {
1349 + <header className="1">
1350 ...
1351 ]",
1367 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1352 ]
1353 `);
1354 });
@@ -1404,7 +1388,6 @@ describe('ReactDOMServerHydration', () => {
1388 - <main className="2">
1389 - <footer className="3">
1390 ]",
1407 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1391 ]
1392 `);
1393 });
@@ -1469,7 +1452,6 @@ describe('ReactDOMServerHydration', () => {
1452 <main>
1453 + <footer className="3">
1454 ]",
1472 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1455 ]
1456 `);
1457 });
@@ -1527,7 +1509,6 @@ describe('ReactDOMServerHydration', () => {
1509 <div className="parent">
1510 - <footer className="3">
1511 ]",
1530 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1512 ]
1513 `);
1514 });
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+131 -67
@@ -234,7 +234,10 @@ describe('ReactDOMServerPartialHydration', () => {
234 suspend = true;
235 ReactDOMClient.hydrateRoot(container, <App />, {
236 onRecoverableError(error) {
237 - Scheduler.log(normalizeError(error.message));
237 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
238 + if (error.cause) {
239 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
240 + }
241 },
242 });
243 await waitForAll([]);
@@ -302,7 +305,10 @@ describe('ReactDOMServerPartialHydration', () => {
305
306 ReactDOMClient.hydrateRoot(container, <App />, {
307 onRecoverableError(error) {
305 - Scheduler.log(normalizeError(error.message));
308 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
309 + if (error.cause) {
310 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
311 + }
312 },
313 });
314 await waitForAll(['Suspend']);
@@ -331,8 +337,7 @@ describe('ReactDOMServerPartialHydration', () => {
337 'Component',
338 'Component',
339 // Hydration mismatch is logged
334 - "Hydration failed because the server rendered HTML didn't match the client.",
335 - 'There was an error while hydrating this Suspense boundary.',
340 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
341 ]);
342
343 // Client rendered - suspense comment nodes removed
@@ -393,7 +398,10 @@ describe('ReactDOMServerPartialHydration', () => {
398
399 ReactDOMClient.hydrateRoot(container, <App />, {
400 onRecoverableError(error) {
396 - Scheduler.log(normalizeError(error.message));
401 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
402 + if (error.cause) {
403 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
404 + }
405 },
406 });
407 await waitForAll(['Suspend']);
@@ -413,8 +421,7 @@ describe('ReactDOMServerPartialHydration', () => {
421 'Component',
422 'Hello',
423 'Component',
416 - "Hydration failed because the server rendered HTML didn't match the client.",
417 - 'There was an error while hydrating this Suspense boundary.',
424 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
425 ]);
426 jest.runAllTimers();
427
@@ -475,7 +482,10 @@ describe('ReactDOMServerPartialHydration', () => {
482
483 ReactDOMClient.hydrateRoot(container, <App />, {
484 onRecoverableError(error) {
478 - Scheduler.log(normalizeError(error.message));
485 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
486 + if (error.cause) {
487 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
488 + }
489 },
490 });
491 await waitForAll(['Suspend']);
@@ -495,8 +505,7 @@ describe('ReactDOMServerPartialHydration', () => {
505 'Component',
506 'Hello',
507 'Component',
498 - "Hydration failed because the server rendered HTML didn't match the client.",
499 - 'There was an error while hydrating this Suspense boundary.',
508 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
509 ]);
510 jest.runAllTimers();
511
@@ -567,7 +576,10 @@ describe('ReactDOMServerPartialHydration', () => {
576
577 ReactDOMClient.hydrateRoot(container, <App />, {
578 onRecoverableError(error) {
570 - Scheduler.log(normalizeError(error.message));
579 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
580 + if (error.cause) {
581 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
582 + }
583 },
584 });
585 await waitForAll(['Component', 'Suspend']);
@@ -587,8 +599,7 @@ describe('ReactDOMServerPartialHydration', () => {
599 'Hello',
600 'Component',
601 'Hello',
590 - "Hydration failed because the server rendered HTML didn't match the client.",
591 - 'There was an error while hydrating this Suspense boundary.',
602 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
603 ]);
604 jest.runAllTimers();
605
@@ -661,7 +672,10 @@ describe('ReactDOMServerPartialHydration', () => {
672
673 ReactDOMClient.hydrateRoot(container, <App />, {
674 onRecoverableError(error) {
664 - Scheduler.log(normalizeError(error.message));
675 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
676 + if (error.cause) {
677 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
678 + }
679 },
680 });
681 await waitForAll([
@@ -669,8 +683,7 @@ describe('ReactDOMServerPartialHydration', () => {
683 'Component',
684 'Suspend',
685 'Fallback',
672 - "Hydration failed because the server rendered HTML didn't match the client.",
673 - 'There was an error while hydrating this Suspense boundary.',
686 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
687 ]);
688 jest.runAllTimers();
689
@@ -740,7 +753,10 @@ describe('ReactDOMServerPartialHydration', () => {
753
754 ReactDOMClient.hydrateRoot(container, <App />, {
755 onRecoverableError(error) {
743 - Scheduler.log(normalizeError(error.message));
756 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
757 + if (error.cause) {
758 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
759 + }
760 },
761 });
762 await waitForAll([
@@ -748,8 +764,7 @@ describe('ReactDOMServerPartialHydration', () => {
764 'Component',
765 'Suspend',
766 'Fallback',
751 - "Hydration failed because the server rendered HTML didn't match the client.",
752 - 'There was an error while hydrating this Suspense boundary.',
767 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
768 ]);
769 jest.runAllTimers();
770
@@ -818,7 +833,10 @@ describe('ReactDOMServerPartialHydration', () => {
833
834 ReactDOMClient.hydrateRoot(container, <App />, {
835 onRecoverableError(error) {
821 - Scheduler.log(normalizeError(error.message));
836 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
837 + if (error.cause) {
838 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
839 + }
840 },
841 });
842 await waitForAll([
@@ -826,8 +844,7 @@ describe('ReactDOMServerPartialHydration', () => {
844 'Component',
845 'Suspend',
846 'Fallback',
829 - "Hydration failed because the server rendered HTML didn't match the client.",
830 - 'There was an error while hydrating this Suspense boundary.',
847 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
848 ]);
849 jest.runAllTimers();
850
@@ -902,7 +919,10 @@ describe('ReactDOMServerPartialHydration', () => {
919
920 ReactDOMClient.hydrateRoot(container, <App />, {
921 onRecoverableError(error) {
905 - Scheduler.log(normalizeError(error.message));
922 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
923 + if (error.cause) {
924 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
925 + }
926 },
927 });
928 await waitForAll([
@@ -910,8 +930,7 @@ describe('ReactDOMServerPartialHydration', () => {
930 'Component',
931 'Suspend',
932 'Fallback',
913 - "Hydration failed because the server rendered HTML didn't match the client.",
914 - 'There was an error while hydrating this Suspense boundary.',
933 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
934 ]);
935 jest.runAllTimers();
936
@@ -994,7 +1013,10 @@ describe('ReactDOMServerPartialHydration', () => {
1013 deleted.push(node);
1014 },
1015 onRecoverableError(error) {
997 - Scheduler.log(normalizeError(error.message));
1016 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
1017 + if (error.cause) {
1018 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
1019 + }
1020 },
1021 });
1022 await waitForAll([]);
@@ -1092,7 +1114,10 @@ describe('ReactDOMServerPartialHydration', () => {
1114 await act(() => {
1115 ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1116 onRecoverableError(error) {
1095 - Scheduler.log(normalizeError(error.message));
1117 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
1118 + if (error.cause) {
1119 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
1120 + }
1121 },
1122 });
1123 });
@@ -1103,8 +1128,7 @@ describe('ReactDOMServerPartialHydration', () => {
1128 assertLog([
1129 'Server rendered',
1130 'Client rendered',
1106 - "Hydration failed because the server rendered HTML didn't match the client.",
1107 - 'There was an error while hydrating this Suspense boundary.',
1131 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
1132 ]);
1133 expect(ref.current).not.toBe(span);
1134 });
@@ -1152,7 +1176,10 @@ describe('ReactDOMServerPartialHydration', () => {
1176 await act(() => {
1177 ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1178 onRecoverableError(error) {
1155 - Scheduler.log(normalizeError(error.message));
1179 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
1180 + if (error.cause) {
1181 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
1182 + }
1183 },
1184 });
1185 });
@@ -1162,8 +1189,7 @@ describe('ReactDOMServerPartialHydration', () => {
1189 });
1190
1191 assertLog([
1165 - "Hydration failed because the server rendered HTML didn't match the client.",
1166 - 'There was an error while hydrating this Suspense boundary.',
1192 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
1193 ]);
1194
1195 expect(container.innerHTML).toContain('<span>A</span>');
@@ -1201,13 +1227,15 @@ describe('ReactDOMServerPartialHydration', () => {
1227 await act(() => {
1228 ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1229 onRecoverableError(error) {
1204 - Scheduler.log(normalizeError(error.message));
1230 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
1231 + if (error.cause) {
1232 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
1233 + }
1234 },
1235 });
1236 });
1237 assertLog([
1209 - "Hydration failed because the server rendered HTML didn't match the client.",
1210 - 'There was an error while hydrating this Suspense boundary.',
1238 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
1239 ]);
1240
1241 expect(container.innerHTML).toContain('<span>A</span>');
@@ -1566,7 +1594,10 @@ describe('ReactDOMServerPartialHydration', () => {
1594 <App text="Hello" className="hello" />,
1595 {
1596 onRecoverableError(error) {
1569 - Scheduler.log(normalizeError(error.message));
1597 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
1598 + if (error.cause) {
1599 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
1600 + }
1601 },
1602 },
1603 );
@@ -1644,7 +1675,10 @@ describe('ReactDOMServerPartialHydration', () => {
1675 <App text="Hello" className="hello" />,
1676 {
1677 onRecoverableError(error) {
1647 - Scheduler.log(normalizeError(error.message));
1678 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
1679 + if (error.cause) {
1680 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
1681 + }
1682 },
1683 },
1684 );
@@ -1720,7 +1754,10 @@ describe('ReactDOMServerPartialHydration', () => {
1754 <App text="Hello" className="hello" />,
1755 {
1756 onRecoverableError(error) {
1723 - Scheduler.log(normalizeError(error.message));
1757 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
1758 + if (error.cause) {
1759 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
1760 + }
1761 },
1762 },
1763 );
@@ -2028,7 +2065,10 @@ describe('ReactDOMServerPartialHydration', () => {
2065 </Context.Provider>,
2066 {
2067 onRecoverableError(error) {
2031 - Scheduler.log(normalizeError(error.message));
2068 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2069 + if (error.cause) {
2070 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2071 + }
2072 },
2073 },
2074 );
@@ -2103,18 +2143,21 @@ describe('ReactDOMServerPartialHydration', () => {
2143 suspend = false;
2144 ReactDOMClient.hydrateRoot(container, <App />, {
2145 onRecoverableError(error) {
2106 - Scheduler.log(normalizeError(error.message));
2146 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2147 + if (error.cause) {
2148 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2149 + }
2150 },
2151 });
2152 if (__DEV__) {
2153 await waitForAll([
2111 - 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2154 + 'onRecoverableError: Switched to client rendering because the server rendering aborted due to:\n\n' +
2155 'The server used' +
2156 ' "renderToString" which does not support Suspense.',
2157 ]);
2158 } else {
2159 await waitForAll([
2117 - 'The server could not finish this Suspense boundary, likely due to ' +
2160 + 'onRecoverableError: The server could not finish this Suspense boundary, likely due to ' +
2161 'an error during server rendering.',
2162 ]);
2163 }
@@ -2173,18 +2216,21 @@ describe('ReactDOMServerPartialHydration', () => {
2216 suspend = false;
2217 ReactDOMClient.hydrateRoot(container, <App />, {
2218 onRecoverableError(error) {
2176 - Scheduler.log(normalizeError(error.message));
2219 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2220 + if (error.cause) {
2221 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2222 + }
2223 },
2224 });
2225 if (__DEV__) {
2226 await waitForAll([
2181 - 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2227 + 'onRecoverableError: Switched to client rendering because the server rendering aborted due to:\n\n' +
2228 'The server used' +
2229 ' "renderToString" which does not support Suspense.',
2230 ]);
2231 } else {
2232 await waitForAll([
2187 - 'The server could not finish this Suspense boundary, likely due to ' +
2233 + 'onRecoverableError: The server could not finish this Suspense boundary, likely due to ' +
2234 'an error during server rendering.',
2235 ]);
2236 }
@@ -2248,18 +2294,21 @@ describe('ReactDOMServerPartialHydration', () => {
2294 suspend = false;
2295 ReactDOMClient.hydrateRoot(container, <App />, {
2296 onRecoverableError(error) {
2251 - Scheduler.log(normalizeError(error.message));
2297 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2298 + if (error.cause) {
2299 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2300 + }
2301 },
2302 });
2303 if (__DEV__) {
2304 await waitForAll([
2256 - 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2305 + 'onRecoverableError: Switched to client rendering because the server rendering aborted due to:\n\n' +
2306 'The server used' +
2307 ' "renderToString" which does not support Suspense.',
2308 ]);
2309 } else {
2310 await waitForAll([
2262 - 'The server could not finish this Suspense boundary, likely due to ' +
2311 + 'onRecoverableError: The server could not finish this Suspense boundary, likely due to ' +
2312 'an error during server rendering.',
2313 ]);
2314 }
@@ -2567,20 +2616,23 @@ describe('ReactDOMServerPartialHydration', () => {
2616
2617 ReactDOMClient.hydrateRoot(container, <App />, {
2618 onRecoverableError(error) {
2570 - Scheduler.log(normalizeError(error.message));
2619 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2620 + if (error.cause) {
2621 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2622 + }
2623 },
2624 });
2625
2626 suspend = true;
2627 if (__DEV__) {
2628 await waitForAll([
2577 - 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2629 + 'onRecoverableError: Switched to client rendering because the server rendering aborted due to:\n\n' +
2630 'The server used' +
2631 ' "renderToString" which does not support Suspense.',
2632 ]);
2633 } else {
2634 await waitForAll([
2583 - 'The server could not finish this Suspense boundary, likely due to ' +
2635 + 'onRecoverableError: The server could not finish this Suspense boundary, likely due to ' +
2636 'an error during server rendering.',
2637 ]);
2638 }
@@ -2640,18 +2692,21 @@ describe('ReactDOMServerPartialHydration', () => {
2692 suspend = false;
2693 ReactDOMClient.hydrateRoot(container, <App />, {
2694 onRecoverableError(error) {
2643 - Scheduler.log(normalizeError(error.message));
2695 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2696 + if (error.cause) {
2697 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2698 + }
2699 },
2700 });
2701 if (__DEV__) {
2702 await waitForAll([
2648 - 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2703 + 'onRecoverableError: Switched to client rendering because the server rendering aborted due to:\n\n' +
2704 'The server used' +
2705 ' "renderToString" which does not support Suspense.',
2706 ]);
2707 } else {
2708 await waitForAll([
2654 - 'The server could not finish this Suspense boundary, likely due to ' +
2709 + 'onRecoverableError: The server could not finish this Suspense boundary, likely due to ' +
2710 'an error during server rendering.',
2711 ]);
2712 }
@@ -2753,7 +2808,10 @@ describe('ReactDOMServerPartialHydration', () => {
2808 </ClassName.Provider>,
2809 {
2810 onRecoverableError(error) {
2756 - Scheduler.log(normalizeError(error.message));
2811 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
2812 + if (error.cause) {
2813 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
2814 + }
2815 },
2816 },
2817 );
@@ -3551,7 +3609,7 @@ describe('ReactDOMServerPartialHydration', () => {
3609 await act(() =>
3610 ReactDOMClient.hydrateRoot(container, <App />, {
3611 onRecoverableError(error) {
3554 - Scheduler.log('Log recoverable error: ' + error.message);
3612 + Scheduler.log('onRecoverableError: ' + error.message);
3613 },
3614 }),
3615 );
@@ -3790,15 +3848,15 @@ describe('ReactDOMServerPartialHydration', () => {
3848 await act(() => {
3849 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
3850 onRecoverableError(error) {
3793 - Scheduler.log(
3794 - 'Log recoverable error: ' + normalizeError(error.message),
3795 - );
3851 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
3852 + if (error.cause) {
3853 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
3854 + }
3855 },
3856 });
3857 });
3858 assertLog([
3800 - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
3801 - 'Log recoverable error: There was an error while hydrating.',
3859 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
3860 ]);
3861
3862 // We show fallback state when mismatch happens at root
@@ -3828,13 +3886,15 @@ describe('ReactDOMServerPartialHydration', () => {
3886 await act(() => {
3887 ReactDOMClient.hydrateRoot(container, <DirectTextChild text="bad" />, {
3888 onRecoverableError(error) {
3831 - Scheduler.log(normalizeError(error.message));
3889 + Scheduler.log('onRecoverableError: ' + normalizeError(error.message));
3890 + if (error.cause) {
3891 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
3892 + }
3893 },
3894 });
3895 });
3896 assertLog([
3836 - "Hydration failed because the server rendered HTML didn't match the client.",
3837 - 'There was an error while hydrating.',
3897 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
3898 ]);
3899 });
3900
@@ -3862,14 +3922,18 @@ describe('ReactDOMServerPartialHydration', () => {
3922 <TextChildWithSibling text="bad" />,
3923 {
3924 onRecoverableError(error) {
3865 - Scheduler.log(normalizeError(error.message));
3925 + Scheduler.log(
3926 + 'onRecoverableError: ' + normalizeError(error.message),
3927 + );
3928 + if (error.cause) {
3929 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
3930 + }
3931 },
3932 },
3933 );
3934 });
3935 assertLog([
3871 - "Hydration failed because the server rendered HTML didn't match the client.",
3872 - 'There was an error while hydrating.',
3936 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
3937 ]);
3938 });
3939 });
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
-1
@@ -477,7 +477,6 @@ describe('ReactDOM HostSingleton', () => {
477 "Hydration failed because the server rendered HTML didn't match the client.",
478 'at div',
479 ],
480 - ['There was an error while hydrating.', null],
480 ]);
481 expect(persistentElements).toEqual([
482 document.documentElement,
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+22 -12
@@ -200,15 +200,17 @@ describe('rendering React components at document', () => {
200 ReactDOMClient.hydrateRoot(container, <div>parsnip</div>, {
201 onRecoverableError: error => {
202 Scheduler.log(
203 - 'Log recoverable error: ' + normalizeError(error.message),
203 + 'onRecoverableError: ' + normalizeError(error.message),
204 );
205 + if (error.cause) {
206 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
207 + }
208 },
209 });
210 });
211
212 assertLog([
210 - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
211 - 'Log recoverable error: There was an error while hydrating.',
213 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
214 ]);
215
216 // This creates an unfortunate double text case.
@@ -229,16 +231,18 @@ describe('rendering React components at document', () => {
231 {
232 onRecoverableError: error => {
233 Scheduler.log(
232 - 'Log recoverable error: ' + normalizeError(error.message),
234 + 'onRecoverableError: ' + normalizeError(error.message),
235 );
236 + if (error.cause) {
237 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
238 + }
239 },
240 },
241 );
242 });
243
244 assertLog([
240 - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
241 - 'Log recoverable error: There was an error while hydrating.',
245 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
246 ]);
247 expect(container.textContent).toBe('parsnip');
248 });
@@ -273,8 +277,13 @@ describe('rendering React components at document', () => {
277 {
278 onRecoverableError: error => {
279 Scheduler.log(
276 - 'Log recoverable error: ' + normalizeError(error.message),
280 + 'onRecoverableError: ' + normalizeError(error.message),
281 );
282 + if (error.cause) {
283 + Scheduler.log(
284 + 'Cause: ' + normalizeError(error.cause.message),
285 + );
286 + }
287 },
288 },
289 );
@@ -291,8 +300,7 @@ describe('rendering React components at document', () => {
300 assertLog(
301 favorSafetyOverHydrationPerf
302 ? [
294 - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
295 - 'Log recoverable error: There was an error while hydrating.',
303 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
304 ]
305 : [],
306 );
@@ -325,15 +333,17 @@ describe('rendering React components at document', () => {
333 {
334 onRecoverableError: error => {
335 Scheduler.log(
328 - 'Log recoverable error: ' + normalizeError(error.message),
336 + 'onRecoverableError: ' + normalizeError(error.message),
337 );
338 + if (error.cause) {
339 + Scheduler.log('Cause: ' + normalizeError(error.cause.message));
340 + }
341 },
342 },
343 );
344 });
345 assertLog([
335 - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
336 - 'Log recoverable error: There was an error while hydrating.',
346 + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
347 ]);
348 expect(testDocument.body.innerHTML).toBe('Hello world');
349 });
packages/react-reconciler/src/ReactCapturedValue.js
+1 -1
@@ -13,7 +13,7 @@ import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
13
14 const CapturedStacks: WeakMap<any, string> = new WeakMap();
15
16 -export type CapturedValue<T> = {
16 +export type CapturedValue<+T> = {
17 +value: T,
18 source: Fiber | null,
19 stack: string | null,
packages/react-reconciler/src/ReactFiberBeginWork.js
+6 -37
@@ -269,7 +269,6 @@ import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent';
269 import {
270 createCapturedValueFromError,
271 createCapturedValueAtFiber,
272 - type CapturedValue,
272 } from './ReactCapturedValue';
273 import {
274 createClassErrorUpdate,
@@ -1500,21 +1499,12 @@ function updateHostRoot(
1499
1500 if (workInProgress.flags & ForceClientRender) {
1501 // Something errored during a previous attempt to hydrate the shell, so we
1503 - // forced a client render.
1504 - const recoverableError = createCapturedValueAtFiber<mixed>(
1505 - new Error(
1506 - 'There was an error while hydrating. Because the error happened outside ' +
1507 - 'of a Suspense boundary, the entire root will switch to ' +
1508 - 'client rendering.',
1509 - ),
1510 - workInProgress,
1511 - );
1502 + // forced a client render. We should have a recoverable error already scheduled.
1503 return mountHostRootWithoutHydrating(
1504 current,
1505 workInProgress,
1506 nextChildren,
1507 renderLanes,
1517 - recoverableError,
1508 );
1509 } else if (nextChildren !== prevChildren) {
1510 const recoverableError = createCapturedValueAtFiber<mixed>(
@@ -1524,12 +1514,12 @@ function updateHostRoot(
1514 ),
1515 workInProgress,
1516 );
1517 + queueHydrationError(recoverableError);
1518 return mountHostRootWithoutHydrating(
1519 current,
1520 workInProgress,
1521 nextChildren,
1522 renderLanes,
1532 - recoverableError,
1523 );
1524 } else {
1525 // The outermost shell has not hydrated yet. Start hydrating.
@@ -1572,13 +1562,10 @@ function mountHostRootWithoutHydrating(
1562 workInProgress: Fiber,
1563 nextChildren: ReactNodeList,
1564 renderLanes: Lanes,
1575 - recoverableError: CapturedValue<mixed>,
1565 ) {
1566 // Revert to client rendering.
1567 resetHydrationState();
1568
1580 - queueHydrationError(recoverableError);
1581 -
1569 workInProgress.flags |= ForceClientRender;
1570
1571 reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -2553,18 +2540,10 @@ function retrySuspenseComponentWithoutHydrating(
2540 current: Fiber,
2541 workInProgress: Fiber,
2542 renderLanes: Lanes,
2556 - recoverableError: CapturedValue<mixed> | null,
2543 ) {
2544 // Falling back to client rendering. Because this has performance
2545 // implications, it's considered a recoverable error, even though the user
2546 // likely won't observe anything wrong with the UI.
2561 - //
2562 - // The error is passed in as an argument to enforce that every caller provide
2563 - // a custom message, or explicitly opt out (currently the only path that opts
2564 - // out is legacy mode; every concurrent path provides an error).
2565 - if (recoverableError !== null) {
2566 - queueHydrationError(recoverableError);
2567 - }
2547
2548 // This will add the old fiber to the deletion list
2549 reconcileChildFibers(workInProgress, current.child, null, renderLanes);
@@ -2688,10 +2667,9 @@ function updateDehydratedSuspenseComponent(
2667 ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
2668 }
2669
2691 - let capturedValue = null;
2670 // TODO: Figure out a better signal than encoding a magic digest value.
2671 if (!enablePostpone || digest !== 'POSTPONE') {
2694 - let error;
2672 + let error: Error;
2673 if (__DEV__ && message) {
2674 // eslint-disable-next-line react-internal/prod-error-codes
2675 error = new Error(message);
@@ -2705,16 +2683,16 @@ function updateDehydratedSuspenseComponent(
2683 // Replace the stack with the server stack
2684 error.stack = (__DEV__ && stack) || '';
2685 (error: any).digest = digest;
2708 - capturedValue = createCapturedValueFromError(
2686 + const capturedValue = createCapturedValueFromError(
2687 error,
2688 componentStack === undefined ? null : componentStack,
2689 );
2690 + queueHydrationError(capturedValue);
2691 }
2692 return retrySuspenseComponentWithoutHydrating(
2693 current,
2694 workInProgress,
2695 renderLanes,
2717 - capturedValue,
2696 );
2697 }
2698
@@ -2795,7 +2773,6 @@ function updateDehydratedSuspenseComponent(
2773 current,
2774 workInProgress,
2775 renderLanes,
2798 - null,
2776 );
2777 } else if (isSuspenseInstancePending(suspenseInstance)) {
2778 // This component is still pending more data from the server, so we can't hydrate its
@@ -2842,21 +2819,13 @@ function updateDehydratedSuspenseComponent(
2819
2820 if (workInProgress.flags & ForceClientRender) {
2821 // Something errored during hydration. Try again without hydrating.
2822 + // The error should've already been logged in throwException.
2823 pushPrimaryTreeSuspenseHandler(workInProgress);
2846 -
2824 workInProgress.flags &= ~ForceClientRender;
2848 - const capturedValue = createCapturedValueFromError(
2849 - new Error(
2850 - 'There was an error while hydrating this Suspense boundary. ' +
2851 - 'Switched to client rendering.',
2852 - ),
2853 - null,
2854 - );
2825 return retrySuspenseComponentWithoutHydrating(
2826 current,
2827 workInProgress,
2828 renderLanes,
2859 - capturedValue,
2829 );
2830 } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) {
2831 // Something suspended and we should still be in dehydrated mode.
packages/react-reconciler/src/ReactFiberHydrationContext.js
+10 -1
@@ -29,6 +29,8 @@ import {
29 } from './ReactWorkTags';
30 import {favorSafetyOverHydrationPerf} from 'shared/ReactFeatureFlags';
31
32 +import {createCapturedValueAtFiber} from './ReactCapturedValue';
33 +
34 import {createFiberFromDehydratedFragment} from './ReactFiber';
35 import {
36 shouldSetTextContent,
@@ -297,6 +299,11 @@ function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
299 return false;
300 }
301
302 +export const HydrationMismatchException: mixed = new Error(
303 + 'Hydration Mismatch Exception: This is not a real error, and should not leak into ' +
304 + "userspace. If you're seeing this, it's likely a bug in React.",
305 +);
306 +
307 function throwOnHydrationMismatch(fiber: Fiber) {
308 let diff = '';
309 if (__DEV__) {
@@ -308,7 +315,7 @@ function throwOnHydrationMismatch(fiber: Fiber) {
315 diff = describeDiff(diffRoot);
316 }
317 }
311 - throw new Error(
318 + const error = new Error(
319 "Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n" +
320 '\n' +
321 "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
@@ -322,6 +329,8 @@ function throwOnHydrationMismatch(fiber: Fiber) {
329 'https://react.dev/link/hydration-mismatch' +
330 diff,
331 );
332 + queueHydrationError(createCapturedValueAtFiber(error, fiber));
333 + throw HydrationMismatchException;
334 }
335
336 function claimHydratableSingleton(fiber: Fiber): void {
packages/react-reconciler/src/ReactFiberThrow.js
+46 -12
@@ -60,6 +60,7 @@ import {
60 } from './ReactFiberSuspenseContext';
61 import {
62 renderDidError,
63 + queueConcurrentError,
64 renderDidSuspendDelayIfPossible,
65 markLegacyErrorBoundaryAsFailed,
66 isAlreadyFailedLegacyErrorBoundary,
@@ -81,6 +82,7 @@ import {
82 getIsHydrating,
83 markDidThrowWhileHydratingDEV,
84 queueHydrationError,
85 + HydrationMismatchException,
86 } from './ReactFiberHydrationContext';
87 import {ConcurrentRoot} from './ReactRootTags';
88 import {noopSuspenseyCommitThenable} from './ReactFiberThenable';
@@ -556,15 +558,55 @@ function throwException(
558
559 // Even though the user may not be affected by this error, we should
560 // still log it so it can be fixed.
559 - queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
561 + if (value !== HydrationMismatchException) {
562 + const wrapperError = new Error(
563 + 'There was an error while hydrating but React was able to recover by ' +
564 + 'instead client rendering from the nearest Suspense boundary.',
565 + {cause: value},
566 + );
567 + queueHydrationError(
568 + createCapturedValueAtFiber(wrapperError, sourceFiber),
569 + );
570 + }
571 + return false;
572 + } else {
573 + if (value !== HydrationMismatchException) {
574 + const wrapperError = new Error(
575 + 'There was an error while hydrating but React was able to recover by ' +
576 + 'instead client rendering the entire root.',
577 + {cause: value},
578 + );
579 + queueHydrationError(
580 + createCapturedValueAtFiber(wrapperError, sourceFiber),
581 + );
582 + }
583 + const workInProgress: Fiber = (root.current: any).alternate;
584 + // Schedule an update at the root to log the error but this shouldn't
585 + // actually happen because we should recover.
586 + workInProgress.flags |= ShouldCapture;
587 + const lane = pickArbitraryLane(rootRenderLanes);
588 + workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
589 + const rootErrorInfo = createCapturedValueAtFiber(value, sourceFiber);
590 + const update = createRootErrorUpdate(
591 + workInProgress.stateNode,
592 + rootErrorInfo, // This should never actually get logged due to the recovery.
593 + lane,
594 + );
595 + enqueueCapturedUpdate(workInProgress, update);
596 + renderDidError();
597 return false;
598 }
599 } else {
600 // Otherwise, fall through to the error path.
601 }
602
566 - value = createCapturedValueAtFiber(value, sourceFiber);
567 - renderDidError(value);
603 + const wrapperError = new Error(
604 + 'There was an error during concurrent rendering but React was able to recover by ' +
605 + 'instead synchronously rendering the entire root.',
606 + {cause: value},
607 + );
608 + queueConcurrentError(createCapturedValueAtFiber(wrapperError, sourceFiber));
609 + renderDidError();
610
611 // We didn't find a boundary that could handle this type of exception. Start
612 // over and traverse parent path again, this time treating the exception
@@ -576,11 +618,11 @@ function throwException(
618 return true;
619 }
620
621 + const errorInfo = createCapturedValueAtFiber(value, sourceFiber);
622 let workInProgress: Fiber = returnFiber;
623 do {
624 switch (workInProgress.tag) {
625 case HostRoot: {
583 - const errorInfo = value;
626 workInProgress.flags |= ShouldCapture;
627 const lane = pickArbitraryLane(rootRenderLanes);
628 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
@@ -593,15 +635,7 @@ function throwException(
635 return false;
636 }
637 case ClassComponent:
596 - if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
597 - // If we're hydrating and got here, it means that we didn't find a suspense
598 - // boundary above so it's a root error. In this case we shouldn't let the
599 - // error boundary capture it because it'll just try to hydrate the error state.
600 - // Instead we let it bubble to the root and let the recover pass handle it.
601 - break;
602 - }
638 // Capture and retry
604 - const errorInfo = value;
639 const ctor = workInProgress.type;
640 const instance = workInProgress.stateNode;
641 if (
packages/react-reconciler/src/ReactFiberWorkLoop.js
+4 -1
@@ -1933,10 +1933,13 @@ export function renderDidSuspendDelayIfPossible(): void {
1933 }
1934 }
1935
1936 -export function renderDidError(error: CapturedValue<mixed>) {
1936 +export function renderDidError() {
1937 if (workInProgressRootExitStatus !== RootSuspendedWithDelay) {
1938 workInProgressRootExitStatus = RootErrored;
1939 }
1940 +}
1941 +
1942 +export function queueConcurrentError(error: CapturedValue<mixed>) {
1943 if (workInProgressRootConcurrentErrors === null) {
1944 workInProgressRootConcurrentErrors = [error];
1945 } else {
scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap
+7
@@ -61,6 +61,13 @@ exports[`error transform should support error constructors with concatenated mes
61 Error(_formatProdErrorMessage(7, foo, bar));"
62 `;
63
64 +exports[`error transform should support extra arguments to error constructor 1`] = `
65 +"import _formatProdErrorMessage from "shared/formatProdErrorMessage";
66 +Error(_formatProdErrorMessage(7, foo, bar), {
67 + cause: error
68 +});"
69 +`;
70 +
71 exports[`error transform should support interpolating arguments with concatenation 1`] = `
72 "import _formatProdErrorMessage from "shared/formatProdErrorMessage";
73 Error(_formatProdErrorMessage(7, foo, bar));"
scripts/error-codes/__tests__/transform-error-messages.js
+8
@@ -154,6 +154,14 @@ let val =
154 (a,
155 // eslint-disable-next-line react-internal/prod-error-codes
156 (b, new Error('foo')));
157 +`)
158 + ).toMatchSnapshot();
159 + });
160 +
161 + it('should support extra arguments to error constructor', () => {
162 + expect(
163 + transform(`
164 +new Error(\`Expected \${foo} target to \` + \`be an array; got \${bar}\`, {cause: error});
165 `)
166 ).toMatchSnapshot();
167 });
scripts/error-codes/codes.json
+5 -3
@@ -407,8 +407,8 @@
407 "419": "The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.",
408 "420": "ServerContext: %s already defined",
409 "421": "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.",
410 - "422": "There was an error while hydrating this Suspense boundary. Switched to client rendering.",
411 - "423": "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.",
410 + "422": "There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.",
411 + "423": "There was an error while hydrating but React was able to recover by instead client rendering the entire root.",
412 "424": "This root received an early update, before anything was able hydrate. Switched the entire root to client rendering.",
413 "425": "Text content does not match server-rendered HTML.",
414 "426": "A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.",
@@ -503,5 +503,7 @@
503 "515": "Cannot assign to a temporary client reference from a server module.",
504 "516": "Attempted to call a temporary Client Reference from the server but it is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.",
505 "517": "Symbols cannot be passed to a Server Function without a temporary reference set. Pass a TemporaryReferenceSet to the options.%s",
506 - "518": "Saw multiple hydration diff roots in a pass. This is a bug in React."
506 + "518": "Saw multiple hydration diff roots in a pass. This is a bug in React.",
507 + "519": "Hydration Mismatch Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React.",
508 + "520": "There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root."
509 }
scripts/error-codes/transform-error-messages.js
+4 -1
@@ -122,7 +122,10 @@ module.exports = function (babel) {
122
123 // Outputs:
124 // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
125 - const newErrorCall = t.callExpression(t.identifier('Error'), [prodMessage]);
125 + const newErrorCall = t.callExpression(t.identifier('Error'), [
126 + prodMessage,
127 + ...node.arguments.slice(1),
128 + ]);
129 newErrorCall[SEEN_SYMBOL] = true;
130 path.replaceWith(newErrorCall);
131 }