react: Make hydration errors more actionable

Hey folks! We at Sentry build error/performance monitoring SDKs and wanted to reach out to see if we could improve the state of hydration errors and make them more actionable. Specifically, we want to look at the stacktraces of hydration errors when you are using production react bundles.

Since React 18 has been getting more adoption, many of our users using React SSR apps have been getting flooded with hydration errors, like listed below:

'https://reactjs.org/docs/error-decoder.html?invariant=422', // There was an error while hydrating this Suspense boundary. Switched to client rendering.
'https://reactjs.org/docs/error-decoder.html?invariant=423', // There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root...
'https://reactjs.org/docs/error-decoder.html?invariant=425'  // Text content does not match server-rendered HTML...

When we discussed this with the community, many users simply wanted to just filter these errors because they were not actionable.

Looking at the implementation in the codebase a simple string error is thrown:

https://github.com/facebook/react/blob/c04b18070145b82111e1162729f4776f4d2c6112/packages/react-reconciler/src/ReactFiberHydrationContext.js#L406

and because of how it is generated the stack trace is often not detailed enough to give users insights about what components/functions were problematic.

For example, here’s a stacktrace I generated from a stock Next.js application with one of these hydration errors:

https://sentry-test.sentry.io/share/issue/b70ea591bbfc4728b33da495148ac9cd/

image

Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
  at Vk(./node_modules/react-dom/cjs/react-dom.production.min.js:280:383)
  at 4448/oF/<(./node_modules/react-dom/cjs/react-dom.production.min.js:280:319)
  at Jk(./node_modules/react-dom/cjs/react-dom.production.min.js:280:319)
  at Ok(./node_modules/react-dom/cjs/react-dom.production.min.js:271:86)
  at Hk(./node_modules/react-dom/cjs/react-dom.production.min.js:268:399)
  at J(./node_modules/scheduler/cjs/scheduler.production.min.js:13:197)
  at R(./node_modules/scheduler/cjs/scheduler.production.min.js:14:126)

As you can see the frames all come from react-dom internals, which means users have no way to start investigating where to look beyond the URL of the page.

In the end we’ve decided to filter these out from default from sentry: https://github.com/getsentry/sentry/issues/45038, but we recognize this is not an ideal solution. Hydration errors are things people should fix, and we want to make it easier for people to fix them!

So with that in mind, are there ways we could make this easier for users? Could we attach a componentStack like we do with error boundaries for these errors? Could we point users to the element that was causing this issue? Any and all ideas/feeback greatly appreciated.

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 45
  • Comments: 17 (4 by maintainers)

Most upvoted comments

These changes only affect dev mode right? I think production hydration errors still have these same data quality issues.

Unfortunately there are a variety of environments where dev mode isn’t good enough to debug this, see this twitter thread as an example.

For folks using Replay, which has DOM snapshots, we can help you debug this with a diff:

https://changelog.getsentry.com/announcements/diff-hydration-errors-with-replay

Heya!

I happened on this whilst helping @slorber look into a hydration issue that has surfaced as Docusaurus migrates to React 18:

https://github.com/facebook/docusaurus/issues/9379#issuecomment-1754450268

We tried plugging in the errorInfo logging in place we got this:

image

main.78700692.js:2 Docusaurus React Root onRecoverableError: Error: Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at ls (main.78700692.js:2:109705)
    at Oc (main.78700692.js:2:182751)
    at xd (main.78700692.js:2:170225)
    at vd (main.78700692.js:2:170197)
    at sd (main.78700692.js:2:165247)
    at w (main.78700692.js:2:232540)
    at MessagePort.T (main.78700692.js:2:233072) {componentStack: '\n    at div\n    at G (http://localhost:3000/assets…calhost:3000/assets/js/main.78700692.js:2:199606)', digest: null}
n @ main.78700692.js:2
(anonymous) @ main.78700692.js:2
wd @ main.78700692.js:2
sd @ main.78700692.js:2
w @ main.78700692.js:2
T @ main.78700692.js:2
main.78700692.js:2 Docusaurus React Root onRecoverableError: Error: Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at ls (main.78700692.js:2:109705)
    at Oc (main.78700692.js:2:182751)
    at xd (main.78700692.js:2:170225)
    at vd (main.78700692.js:2:170197)
    at sd (main.78700692.js:2:165247)
    at w (main.78700692.js:2:232540)
    at MessagePort.T (main.78700692.js:2:233072) {componentStack: '\n    at div\n    at q (http://localhost:3000/assets…calhost:3000/assets/js/main.78700692.js:2:199606)', digest: null}
n @ main.78700692.js:2
(anonymous) @ main.78700692.js:2
wd @ main.78700692.js:2
sd @ main.78700692.js:2
w @ main.78700692.js:2
T @ main.78700692.js:2
main.78700692.js:2 Docusaurus React Root onRecoverableError: Error: Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at ls (main.78700692.js:2:109705)
    at Oc (main.78700692.js:2:182751)
    at xd (main.78700692.js:2:170225)
    at vd (main.78700692.js:2:170197)
    at sd (main.78700692.js:2:165247)
    at w (main.78700692.js:2:232540)
    at MessagePort.T (main.78700692.js:2:233072) {componentStack: '\n    at footer\n    at kn (http://localhost:3000/as…calhost:3000/assets/js/main.78700692.js:2:199606)', digest: null}
n @ main.78700692.js:2
(anonymous) @ main.78700692.js:2
wd @ main.78700692.js:2
sd @ main.78700692.js:2
w @ main.78700692.js:2
T @ main.78700692.js:2
main.78700692.js:2 Docusaurus React Root onRecoverableError: Error: Minified React error #423; visit https://reactjs.org/docs/error-decoder.html?invariant=423 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at Oc (main.78700692.js:2:182437)
    at xd (main.78700692.js:2:170225)
    at yd (main.78700692.js:2:170153)
    at hd (main.78700692.js:2:170016)
    at id (main.78700692.js:2:166823)
    at sd (main.78700692.js:2:165375)
    at w (main.78700692.js:2:232540)
    at MessagePort.T (main.78700692.js:2:233072) {componentStack: '', digest: null}

It’s more information than we had, but it’s still not very actionable. Consider the componentStack

\n    at div\n    at G (http://localhost:3000/assets/js/main.78700692.js:2:624664)\n    at Bt (http://localhost:3000/assets/js/main.78700692.js:2:661953)\n    at nav\n    at qt (http://localhost:3000/assets/js/main.78700692.js:2:663366)\n    at sn\n    at q (http://localhost:3000/assets/js/main.78700692.js:2:623699)\n    at p (http://localhost:3000/assets/js/main.78700692.js:2:264647)\n    at r (http://localhost:3000/assets/js/main.78700692.js:2:264973)\n    at http://localhost:3000/assets/js/main.78700692.js:2:672280\n    at g (http://localhost:3000/assets/js/main.78700692.js:2:275178)\n    at b (http://localhost:3000/assets/js/main.78700692.js:2:275363)\n    at y (http://localhost:3000/assets/js/main.78700692.js:2:262590)\n    at v (http://localhost:3000/assets/js/main.78700692.js:2:262676)\n    at l (http://localhost:3000/assets/js/main.78700692.js:2:277407)\n    at b (http://localhost:3000/assets/js/main.78700692.js:2:258340)\n    at f (http://localhost:3000/assets/js/main.78700692.js:2:259271)\n    at http://localhost:3000/assets/js/main.78700692.js:2:276403\n    at En (http://localhost:3000/assets/js/main.78700692.js:2:672394)\n    at Nn (http://localhost:3000/assets/js/main.78700692.js:2:673299)\n    at Ln\n    at v (http://localhost:3000/assets/js/ccc49370.aa459f07.js:1:21579)\n    at g (http://localhost:3000/assets/js/ccc49370.aa459f07.js:1:33004)\n    at g (http://localhost:3000/assets/js/main.78700692.js:2:275178)\n    at l (http://localhost:3000/assets/js/ccc49370.aa459f07.js:1:59429)\n    at h (http://localhost:3000/assets/js/ccc49370.aa459f07.js:1:33429)\n    at c (http://localhost:3000/assets/js/main.78700692.js:2:373197)\n    at n (http://localhost:3000/assets/js/main.78700692.js:2:208520)\n    at t (http://localhost:3000/assets/js/main.78700692.js:2:218284)\n    at t (http://localhost:3000/assets/js/main.78700692.js:2:219232)\n    at t (http://localhost:3000/assets/js/main.78700692.js:2:218284)\n    at M (http://localhost:3000/assets/js/main.78700692.js:2:288684)\n    at z (http://localhost:3000/assets/js/main.78700692.js:2:290015)\n    at g (http://localhost:3000/assets/js/main.78700692.js:2:285165)\n    at i (http://localhost:3000/assets/js/main.78700692.js:2:284800)\n    at p (http://localhost:3000/assets/js/main.78700692.js:2:363422)\n    at b (http://localhost:3000/assets/js/main.78700692.js:2:365297)\n    at Q (http://localhost:3000/assets/js/main.78700692.js:2:292784)\n    at t (http://localhost:3000/assets/js/main.78700692.js:2:216498)\n    at t (http://localhost:3000/assets/js/main.78700692.js:2:211808)\n    at t (http://localhost:3000/assets/js/main.78700692.js:2:199606)

It’s not obvious what this reveals, beyond that a div is involved. Are there other steps we should be following to get more meaningful output perhaps?

The above was obtained by:

git clone https://github.com/facebook/docusaurus.git
cd docusaurus
yarn install
yarn build
yarn serve

Browse to http://localhost:3000/ and opening devtools to look at the console.

Interestingly (and not related to this) we’ve been seeing errors that only seem to surface on Ubuntu which is intriguing.

@gaearon apologize for the delay, had gone on vacation last week!

made a quick example to help address some of the points you made: https://github.com/AbhiPrasad/nextjs-hydration-error-example using a nextjs app.

I had to monkeypatch onRecoverableError since next does not expose this, so please excuse that part of the code 😅.

Aren’t these built-in ones like etc? You should be able to map the actual React components further in the stack. Yes, this wouldn’t tell the exact location, but would at least narrow it down to the component function.

You’re right that they mostly help. We generally get a componentStack like so in prod builds:

div
main
p
i@http://localhost:3000/_next/static/chunks/pages/_app-53818a55c8835939.js:14:12349
6985/t.PathnameContextProviderAdapter@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:42869
G@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:7600
Y@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:9138
ea@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:11913

This + the URL of the page is usually good enough to figure out where the issue is. The problem is they aren’t solely the built-in components all the time, so we get minified React component names in there for more complex use cases, which is still pretty confusing for users.

p
div
m // minified name
div
f // minified name
div
h // minified name
main
p
i@http://localhost:3000/_next/static/chunks/pages/_app-53818a55c8835939.js:14:12349
6985/t.PathnameContextProviderAdapter@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:42869
G@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:7600
Y@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:9138
ea@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:11913

Or maybe we should patch it onto the object. We already patch err.digest on main. I’ll ask if we considered doing the same for .componentStack and why (not).

This would be great!

Can you provide a repro case? I could have a look.

When running https://github.com/AbhiPrasad/nextjs-hydration-error-example in production mode, invariant 423 does not generate a componentStack (message: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.)

console.log(error);
console.log(errorInfo);
console.log(errorInfo.componentStack);

image

It’s not obvious to me why the frames in the stack you mentioned aren’t proper. They are reconstructed from actual stack lines merged together so they should give you React component functions. The only missing parts are components like <div>. What am I missing?

This is partly a Sentry consequence, since we need the filename and line/col number to do source mapping, which helps with the case where there are minified component names in the stacktrace. Something like G@http://localhost:3000/_next/static/chunks/main-b1241a9a70bb7dcd.js:1:7600 for example.

But we can live without this for now!

Finally, when you generate a hydration issue, you get 3 errors thrown:

  1. 425: Text content does not match server-rendered HTML.
  2. 418: Hydration failed because the initial UI does not match what was rendered on the server.
  3. 423: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

I understand why all of these errors must be thrown (they all signal different parts of the problem), but IMO the experience of getting all of these in the console/in your production error monitoring system is confusing and can be overwhelming for users, especially those on the more junior side. Perhaps we can think of a way of reducing the amount of errors thrown?

Or maybe we should patch it onto the object. We already patch err.digest on main. I’ll ask if we considered doing the same for .componentStack and why (not).

Having componentStack as a property on hydration errors would be really cool and useful.

Even though it would also be cool to have file/lineno/col for individual tags I don’t think it is absolutely necessary - without it the component stack is already useful enough. Implementing this would probably mean having to patch the stack trace together somehow using synthetic errors which is probably quite expensive.

Appreciate the quick response!

I believe you should be receiving those in the onRecoverableError callback for hydrateRoot as the second argument

So this works pretty decently to a certain extent, we just have to convince framework authors to expose these hooks more easily, as it’s better than nothing.

For 425 and 418, we get componentStacks:

a
div
i
u@http://0.0.0.0:3000/_next/static/chunks/main-705a528c766ed2ff.js:1:33240
401/t.PathnameContextProviderAdapter@http://0.0.0.0:3000/_next/static/chunks/main-705a528c766ed2ff.js:1:42849
G@http://0.0.0.0:3000/_next/static/chunks/main-705a528c766ed2ff.js:1:7595
Y@http://0.0.0.0:3000/_next/static/chunks/main-705a528c766ed2ff.js:1:9134
ea@http://0.0.0.0:3000/_next/static/chunks/main-705a528c766ed2ff.js:1:11907

So here a, div, and i are react components, which should help a lot with debugging. The only issue is that they don’t have function names/line and col numbers - so we can’t apply sourcemaps to transform these into readable component names. Of course people can set a displayName, but that’s not realistic for a bigger application.

Here’s some basic boilerplate that will work to send this stuff to an error monitoring service:

function onRecoverableError(error, errInfo) {
 let context = {};

  if (errInfo && errInfo.componentStack) {
     // generating this synthetic error allows services like Sentry to apply sourcemaps
     // to unminify the stacktrace and make it readable.
     const errorBoundaryError = new Error(error.message);
     errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`;
     errorBoundaryError.stack = errorInfo.componentStack;

     // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
     error.cause = errorBoundaryError

     // you can also just add the plain text as some kind of added event context
     context.react = {
       componentStack: errorInfo.componentStack
     }
  }
 // feel free to replace with your favourite error monitoring service
 Sentry.captureException(error, { context })
}

It seems like 423 does not generate a componentStack though. Given though that the other invariants are always thrown with this, I think it’s fine.

So with that in mind two immediate items:

  1. We have to convince framework authors to expose onRecoverableError.

Seems like this has been a feature request in Next.js, and it seems Astro can also improve here. Remix thankfully seems to be super transparent with exposing hydrateRoot. Gatsby also allows for this. We at Sentry can help talk to these folks!

  1. We need better frames for the react components in the componentStack.

Without proper frames, we can never apply sourcemaps, so users are stuck with minified component names.

As for anything else, I’m trying to brainstorm what other tools we can give to people to help identify what to fix here. Ideally most folks just want the line of code that they need to change.

If bundle size is a concern from adding new logic, perhaps we can add an API that returns a errored response to the server that CSR had to occur. Then the server could report many details about what kind of HTML it tried to serve, and as along as you can trace the error somehow between backend/frontend, you might be able to debug this faster.

Seems like Next.js 13.2 has improved their error overlays and local debugging experience for these - https://github.com/facebook/react/issues/24519#issuecomment-1439915463

https://github.com/vercel/next.js/pull/45089 seems to be this PR

Edit: These are dev only - which still means the production stacktraces still have the same problem - which is what we are looking to improve here.