Skip to content

PATTERN Cited by 1 source

First-error-only hydration error reporting

Pattern. When reporting React 18 hydration mismatches to an error tracker (Sentry, Datadog, Rollbar, custom), forward only the first onRecoverableError call per hydration pass. Drop subsequent ones silently — they are usually false-positive consequences of the first mismatch, not independent bugs.

Why

React 18's hydration walk compares a list of server-rendered DOM nodes against a list of client-rendered React elements (fibers) and attempts to pair them. When one pair fails to match, onRecoverableError fires — but the list alignment is now broken. React continues the walk trying to hydrate the remaining nodes, but because the lists are misaligned, many subsequent pairings also fail. Each one calls onRecoverableError again, and each is a consequence, not a root cause.

The Zalando Rendering Engine post documents this explicitly:

"Another issue we encountered was that the onRecoverableError callback is usually called multiple times by React for a single hydration mismatch problem, both polluting our Sentry logs as well as making the debugging process harder. This seems to be due to the way hydration phase works, in which React compares a list of available server rendered DOM nodes with a list of client rendered React elements ('fibers') and tries to match them together and basically hydrate the nodes. And when matching and hydration fails for a specific node instance and errors are logged, it tries to hydrate the next one. What we observed here was that (at least in some cases) because of the previous mismatching node/fiber, the order of the lists becomes broken, and that leads to all the next ones failing as well. And that means a lot of other hydration mismatch error logs which aren't necessarily correct." (Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react.)

"To mitigate this in the production environment, we modified our error tracking code to only send the first hydration error log to Sentry."

Shape

let hydrationErrorsReportedThisSession = 0;

hydrateRoot(container, element, {
  onRecoverableError(error, errorInfo) {
    if (!isHydrationMismatch(error)) {
      // Other recoverable errors — always report.
      sentry.captureException(error, { extra: errorInfo });
      return;
    }

    if (hydrationErrorsReportedThisSession > 0) {
      // Probably a cascade — drop.
      return;
    }

    hydrationErrorsReportedThisSession++;
    sentry.captureException(error, {
      extra: errorInfo,
      tags: { hydration_mismatch: true, first_of_session: true },
    });
  },
});

What counts as "the session"

  • Page load, not user session. Each navigation-hydration is one cascade.
  • Per top-level hydrateRoot call — if you have multiple (per micro-frontend), each gets its own counter.
  • Reset between navigations (if you rehydrate on client-side navigation).

Trade-offs

Wins

  • Sentry signal-to-noise massively improves. Mismatch root causes become visible instead of buried under cascade logs.
  • Debugging is easier — the first reported fiber is (usually) the cause; subsequent ones are noise.
  • Cost / quota pressure on the error-tracking backend drops — hydration cascades can produce 10s–100s of events per single real mismatch.

Costs

  • You can miss secondary root causes. If two unrelated mismatches happen in the same hydration pass (rare but possible), you'll only see one. Mitigation: run periodic dev-mode sampling (e.g. 1 in 10000 sessions) that does log all errors and flags sessions with unusually-many cascaded errors for manual inspection.
  • Can mask regressions. If two different components both start mismatching in the same deploy, only one shows up. Mitigation: tag the first error with a "cascade size" counter so Sentry can group events by cascade-size outlier.
  • Only applicable to hydration-mismatch-class errors. Don't dedupe general recoverable errors this way.

Variant: dedup by error signature

Instead of just "first error of the session," dedupe by:

  • The minified component name + error-message hash, so distinct mismatches across the session are kept but repeats of the same one are dropped.
  • Sample rate — send 1 of N of each duplicate for fleet-wide signal.

When not to use

  • React 18 alpha / experimental builds — the cascade behaviour has changed between minor versions; verify first-error-only still corresponds to the root cause.
  • React 19+ — verify behaviour post-React-19, which fixed several classes of hydration regression. The dedup may still be required for the cascade property but some mismatch categories might no longer produce it.
  • Dev mode — dev-mode errors are separate from onRecoverableError; dev ergonomics are unchanged.

Observability implications

The wider lesson: error-tracker volume is an observability signal of its own. Zalando's move from "log everything" to "log first only" is a pattern about what to do when a single root cause produces 10s of error events in a cascade. It applies beyond hydration mismatches — any class of error where downstream failures are caused by the first one (schema- migration failures, chained RPC timeouts, connection-pool exhaustion) benefits from the same dedup.

Seen in

Last updated · 501 distilled / 1,218 read