Skip to content

fix(hydration): route mismatches via handleError when consumer set#14757

Open
pierluigilenoci wants to merge 1 commit into
vuejs:mainfrom
pierluigilenoci:fix/hydration-error-handler
Open

fix(hydration): route mismatches via handleError when consumer set#14757
pierluigilenoci wants to merge 1 commit into
vuejs:mainfrom
pierluigilenoci:fix/hydration-error-handler

Conversation

@pierluigilenoci
Copy link
Copy Markdown

@pierluigilenoci pierluigilenoci commented Apr 24, 2026

Summary

Hydration mismatch errors were never routed through Vue's error handling pipeline, so onErrorCaptured and app.config.errorHandler could not catch them. This made it impossible to wire hydration-mismatch reporting into observability tools (Sentry, Datadog, etc.) — see issue #13154.

This PR routes hydration mismatches through handleError, but only when an explicit consumer is present, so SSR apps that have not opted in see no behavior change.

Behavior

logMismatchError calls handleError(..., ErrorCodes.HYDRATION_MISMATCH, false) only when one of the following is true on the parent component instance:

  1. instance.appContext.config.errorHandler is set, or
  2. some ancestor component has registered onErrorCaptured.

Without a consumer, the per-mismatch warn() calls at the call sites remain the only output — matching the prior default behavior. No "Unhandled error during execution of hydration" warning is emitted for SSR apps that have not opted in.

Changes

  • packages/runtime-core/src/errorHandling.ts — adds ErrorCodes.HYDRATION_MISMATCH and its 'hydration' info label.
  • packages/runtime-core/src/hydration.tslogMismatchError now takes the parent component instance, gates handleError on consumer presence (errorHandler or any ancestor with onErrorCaptured), and is invoked with parentComponent at every mismatch call site.
  • packages/runtime-core/__tests__/hydration.spec.ts — adds 4 tests covering: app.config.errorHandler consumer, onErrorCaptured consumer, single-emit semantics across multiple mismatches, and the no-consumer path (asserts the unhandled-error warning is not emitted).

Reviewer feedback addressed

@edison1105 raised concerns about semantics changes for apps that had already configured errorHandler/onErrorCaptured, and about the unhandled-error warning fallback for default apps. The current revision keeps default behavior unchanged and only opens the integration to apps that have explicitly opted in by registering a consumer.

Test plan

  • pnpm vitest run packages/runtime-core/__tests__/hydration.spec.ts — 108 tests pass
  • pnpm lint — clean
  • pnpm check (tsc) — clean
  • DCO sign-off on commit
  • Single commit, rebased onto current main

Closes #13154.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Hydration mismatch reporting is routed through Vue's error pipeline: add ErrorCodes.HYDRATION_MISMATCH, implement logMismatchError(instance) with a dedupe guard and resetHydrationMismatchState(), update mismatch call sites to pass component context, and add tests verifying handlers receive the error.

Changes

Hydration mismatch handling

Layer / File(s) Summary
Error code mapping
packages/runtime-core/src/errorHandling.ts
Added HYDRATION_MISMATCH to ErrorCodes and mapped it to 'hydration' in ErrorTypeStrings.
Hydration core behavior
packages/runtime-core/src/hydration.ts
Added logMismatchError(instance) guarded by hasLoggedMismatchError, hasErrorCaptured(instance) helper, routing via handleError(..., ErrorCodes.HYDRATION_MISMATCH), and exported resetHydrationMismatchState().
Call-site updates
packages/runtime-core/src/hydration.ts
Updated hydration mismatch call sites to pass parentComponent into logMismatchError(parentComponent) across multiple mismatch paths.
Test coverage
packages/runtime-core/__tests__/hydration.spec.ts
Imported onErrorCaptured and resetHydrationMismatchState; added tests ensuring app and component error handlers receive the hydration mismatch Error, that it’s emitted once, and warnings behave without handlers.

Sequence Diagram

sequenceDiagram
  participant Hydration as HydrationSystem
  participant Log as logMismatchError
  participant Handler as handleError
  participant App as app.config.errorHandler
  participant Comp as onErrorCaptured

  Hydration->>Log: detect mismatch (parentComponent)
  Log->>Log: check hasLoggedMismatchError
  alt not logged
    Log->>Log: set hasLoggedMismatchError = true
    Log->>Handler: handleError(Error("Hydration completed but contains mismatches."), instance, HYDRATION_MISMATCH)
    Handler->>App: invoke if configured
    Handler->>Comp: invoke if ancestor captured error
  else already logged
    Log->>Log: skip duplicate emission
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • vuejs/core#14833: Also modifies hydration-mismatch reporting and the conditions under which logMismatchError is invoked.

Suggested labels

ready to merge, :hammer: p3-minor-bug

Suggested reviewers

  • Doctor-wu
  • johnsoncodehk

Poem

🐰 I hopped through code at break of dawn,
Brought mismatches into handlers spawned.
With a guard and reset, we log just once,
Errors now bubble to the app and the branch.
Hop on, devs — catch them at a glance.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(hydration): route mismatches via handleError when consumer set' accurately describes the main change: routing hydration mismatches through the error handling pipeline when error consumers are present.
Linked Issues check ✅ Passed The PR fulfills issue #13154's objectives: hydration mismatches now route through Vue's error pipeline (onErrorCaptured and app.config.errorHandler) when consumers exist, providing richer error context while preserving default behavior for apps without explicit error consumers.
Out of Scope Changes check ✅ Passed All changes are directly scoped to resolving issue #13154: adding HYDRATION_MISMATCH error code, implementing conditional handleError routing, and adding corresponding test coverage for the new error handling behavior.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/runtime-core/src/hydration.ts`:
- Around line 68-92: The current call to handleError(...) always routes through
logError() and emits extra dev warnings; change the logic so handleError is only
invoked when there is an actual consumer (check
instance.appContext.config.errorHandler or hasErrorCaptured(instance)), and when
no consumer exists fall back to directly calling console.error(new
Error('Hydration completed but contains mismatches.')) instead of handleError;
update both the __TEST__ branch and the default branch to use this gating around
handleError and preserve the existing ErrorCodes.HYDRATION_MISMATCH constant
when routing to handleError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4511270-f4c7-4adc-aa7e-d07fa84ea855

📥 Commits

Reviewing files that changed from the base of the PR and between 3310eea and 5e1c9ff.

📒 Files selected for processing (3)
  • packages/runtime-core/__tests__/hydration.spec.ts
  • packages/runtime-core/src/errorHandling.ts
  • packages/runtime-core/src/hydration.ts

Comment thread packages/runtime-core/src/hydration.ts Outdated
Comment on lines +68 to +92
// Route through Vue's error handling pipeline so that
// onErrorCaptured and app.config.errorHandler can catch it.
if (__TEST__) {
// In test mode, only route through handleError if an error handler is
// actually configured (errorHandler or onErrorCaptured), to avoid adding
// unexpected "Unhandled error" warnings to every mismatch test.
if (
instance &&
(instance.appContext.config.errorHandler || hasErrorCaptured(instance))
) {
handleError(
new Error('Hydration completed but contains mismatches.'),
instance,
ErrorCodes.HYDRATION_MISMATCH,
false,
)
}
} else {
handleError(
new Error('Hydration completed but contains mismatches.'),
instance,
ErrorCodes.HYDRATION_MISMATCH,
false,
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Preserve the old fallback when no error handler is present.

handleError(..., false) still goes through logError(), which adds a new dev warning (Unhandled error during execution of hydration) before console.error(err). That means unhandled hydration mismatches now emit extra generic noise in dev, even though the old behavior was just the specific mismatch warning plus the one-off console error.

Please gate the handleError() path on an actual consumer (app.config.errorHandler / ancestor errorCaptured) and keep the old direct console.error(new Error(...)) fallback otherwise.

Proposed fix
 const logMismatchError = (
   instance: ComponentInternalInstance | null = null,
 ) => {
   if (hasLoggedMismatchError) {
     return
   }
   hasLoggedMismatchError = true

-  // Route through Vue's error handling pipeline so that
-  // onErrorCaptured and app.config.errorHandler can catch it.
-  if (__TEST__) {
-    // In test mode, only route through handleError if an error handler is
-    // actually configured (errorHandler or onErrorCaptured), to avoid adding
-    // unexpected "Unhandled error" warnings to every mismatch test.
-    if (
-      instance &&
-      (instance.appContext.config.errorHandler || hasErrorCaptured(instance))
-    ) {
-      handleError(
-        new Error('Hydration completed but contains mismatches.'),
-        instance,
-        ErrorCodes.HYDRATION_MISMATCH,
-        false,
-      )
-    }
-  } else {
-    handleError(
-      new Error('Hydration completed but contains mismatches.'),
-      instance,
-      ErrorCodes.HYDRATION_MISMATCH,
-      false,
-    )
-  }
+  const err = new Error('Hydration completed but contains mismatches.')
+  const hasConsumer =
+    !!instance &&
+    (instance.appContext.config.errorHandler || hasErrorCaptured(instance))
+
+  if (hasConsumer) {
+    handleError(err, instance, ErrorCodes.HYDRATION_MISMATCH, false)
+  } else if (!__TEST__) {
+    console.error(err)
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Route through Vue's error handling pipeline so that
// onErrorCaptured and app.config.errorHandler can catch it.
if (__TEST__) {
// In test mode, only route through handleError if an error handler is
// actually configured (errorHandler or onErrorCaptured), to avoid adding
// unexpected "Unhandled error" warnings to every mismatch test.
if (
instance &&
(instance.appContext.config.errorHandler || hasErrorCaptured(instance))
) {
handleError(
new Error('Hydration completed but contains mismatches.'),
instance,
ErrorCodes.HYDRATION_MISMATCH,
false,
)
}
} else {
handleError(
new Error('Hydration completed but contains mismatches.'),
instance,
ErrorCodes.HYDRATION_MISMATCH,
false,
)
}
const logMismatchError = (
instance: ComponentInternalInstance | null = null,
) => {
if (hasLoggedMismatchError) {
return
}
hasLoggedMismatchError = true
const err = new Error('Hydration completed but contains mismatches.')
const hasConsumer =
!!instance &&
(instance.appContext.config.errorHandler || hasErrorCaptured(instance))
if (hasConsumer) {
handleError(err, instance, ErrorCodes.HYDRATION_MISMATCH, false)
} else if (!__TEST__) {
console.error(err)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/runtime-core/src/hydration.ts` around lines 68 - 92, The current
call to handleError(...) always routes through logError() and emits extra dev
warnings; change the logic so handleError is only invoked when there is an
actual consumer (check instance.appContext.config.errorHandler or
hasErrorCaptured(instance)), and when no consumer exists fall back to directly
calling console.error(new Error('Hydration completed but contains mismatches.'))
instead of handleError; update both the __TEST__ branch and the default branch
to use this gating around handleError and preserve the existing
ErrorCodes.HYDRATION_MISMATCH constant when routing to handleError.

@pierluigilenoci
Copy link
Copy Markdown
Author

Hi — friendly ping. Is this PR still on the radar for review? Happy to rebase or make changes if needed. Thanks!

@edison1105
Copy link
Copy Markdown
Member

I agree the observability use case is valid, but I don't think this should be merged in the current form.

Routing hydration mismatch reporting through handleError() changes the existing semantics: onErrorCaptured can now stop propagation, existing app.config.errorHandler hooks will start receiving hydration mismatches, and the fallback console behavior changes when there is no explicit consumer. So this is more of a minor feature than a bug fix.

I think the safer direction is to preserve the current default behavior and only expose hydration mismatch reporting to user handlers when there is an explicit consumer, or consider an app-level / warning-handler based API instead of putting it fully into the component error boundary pipeline.

@pierluigilenoci
Copy link
Copy Markdown
Author

Hi — friendly follow-up. CI is green and all checks pass. Would you be able to review when you get a chance? Thank you!

@pierluigilenoci pierluigilenoci force-pushed the fix/hydration-error-handler branch from 5e1c9ff to 81d3a55 Compare May 22, 2026 09:14
@pierluigilenoci
Copy link
Copy Markdown
Author

@edison1105 thanks for the careful review — fully agree. I just pushed a rework that addresses your concerns:

handleError is now gated on an explicit consumer. logMismatchError only routes through handleError(..., HYDRATION_MISMATCH, false) when one of the following is true on the parent component instance:

  1. appContext.config.errorHandler is set, or
  2. some ancestor component has registered onErrorCaptured.

Without a consumer, the per-mismatch warn() calls at the call sites remain the only output — which is exactly the prior default behavior. SSR apps that have not opted in see no "Unhandled error during execution of hydration" warning and no behavior change at all. Apps that have opted in (errorHandler / onErrorCaptured) get the integration with the error pipeline that issue #13154 asked for.

Other concerns:

  • onErrorCaptured stopping propagation: only relevant when an ancestor has opted in by registering it. The default-no-consumer path is unchanged.
  • Existing app.config.errorHandler hooks suddenly receiving hydration mismatches: fair point as a semantics change, but it requires the user to have explicitly set errorHandler and be doing SSR with mismatches; in that narrow case the new signal is what observability users ("Hydration completed but contains mismatches" error not caught by the Vue error handler #13154) want. Happy to add a release-note entry if you'd like.

Tests cover all four cases: errorHandler consumer, onErrorCaptured consumer, single-emit guard, and the no-consumer path (asserts "Unhandled error during execution of hydration" is not warned).

Let me know if the gating-on-consumer shape works for you, or if you'd prefer the app-level / warning-handler API instead — happy to iterate.

@pierluigilenoci pierluigilenoci changed the title fix(hydration): route hydration mismatch errors through handleError (fix #13154) fix(hydration): route hydration mismatch errors through handleError when a consumer is set (fix #13154) May 22, 2026
@pierluigilenoci pierluigilenoci changed the title fix(hydration): route hydration mismatch errors through handleError when a consumer is set (fix #13154) fix(hydration): route mismatches via handleError when consumer set May 22, 2026
Hydration mismatch errors were never routed through Vue's error
handling pipeline, so onErrorCaptured and app.config.errorHandler
could not catch them — making it hard to wire hydration mismatch
reporting into observability tools (fix vuejs#13154).

logMismatchError now calls handleError(..., HYDRATION_MISMATCH, false)
only when an explicit consumer is present:

  - app.config.errorHandler is set, or
  - some ancestor has registered onErrorCaptured.

Without a consumer, the per-mismatch warn() calls at the call sites
remain the only output, matching the prior default behavior — no
'Unhandled error during execution of hydration' warning is emitted
for SSR apps that have not opted in.

Adds ErrorCodes.HYDRATION_MISMATCH plus its 'hydration' info label,
passes the parent component instance to logMismatchError() at every
call site, and adds tests covering: app.config.errorHandler consumer,
onErrorCaptured consumer, single-emit semantics across multiple
mismatches, and the no-consumer path.

Signed-off-by: Pierluigi Lenoci <pierluigilenoci@gmail.com>
@pierluigilenoci pierluigilenoci force-pushed the fix/hydration-error-handler branch from 81d3a55 to 68656d1 Compare May 22, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Hydration completed but contains mismatches" error not caught by the Vue error handler

2 participants