fix(hydration): route hydration mismatch errors through handleError (fix #13154)#14757
fix(hydration): route hydration mismatch errors through handleError (fix #13154)#14757pierluigilenoci wants to merge 1 commit intovuejs:mainfrom
Conversation
vuejs#13154) Hydration mismatch errors were using `console.error` directly, bypassing Vue's error handling pipeline. This meant `onErrorCaptured` and `app.config.errorHandler` could not catch these errors, making it impossible to integrate hydration mismatch reporting with observability tools. This change: - Adds `ErrorCodes.HYDRATION_MISMATCH` to the error code enum - Routes hydration mismatch errors through `handleError()` instead of `console.error()`, so they flow through `onErrorCaptured` hooks and `app.config.errorHandler` - Passes the parent component instance to `logMismatchError()` at all call sites to provide proper component context - Preserves backward compatibility: when no error handler is configured, the error still surfaces via `console.error` through `handleError`'s default fallback - Adds tests verifying both `app.config.errorHandler` and `onErrorCaptured` can intercept hydration mismatch errors Signed-off-by: Pierluigi Lenoci <pierluigilenoci@gmail.com>
📝 WalkthroughWalkthroughThe changes centralize hydration mismatch error reporting through Vue's error handling pipeline instead of direct console output. A new error code Changes
Sequence DiagramsequenceDiagram
participant Hydration as Hydration System
participant ErrorLog as logMismatchError()
participant Handler as handleError()
participant AppHandler as app.config.errorHandler
participant Component as onErrorCaptured (Component)
Hydration->>ErrorLog: mismatch detected<br/>(instance, parentComponent)
ErrorLog->>ErrorLog: check hasLoggedMismatchError guard
alt Guard not set
ErrorLog->>ErrorLog: set hasLoggedMismatchError = true
ErrorLog->>Handler: handleError(Error, instance,<br/>code: HYDRATION_MISMATCH)
Handler->>AppHandler: invoke if configured
AppHandler-->>Handler: error handled/propagated
Handler->>Component: invoke if ancestor<br/>has onErrorCaptured
Component-->>Handler: error handled/propagated
else Guard already set
ErrorLog->>ErrorLog: skip (already logged once)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
packages/runtime-core/__tests__/hydration.spec.tspackages/runtime-core/src/errorHandling.tspackages/runtime-core/src/hydration.ts
| // 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, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| // 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.
|
Hi — friendly ping. Is this PR still on the radar for review? Happy to rebase or make changes if needed. Thanks! |
|
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 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. |
Summary
Hydration mismatch errors were using
console.errordirectly, bypassing Vue's error handling pipeline. This meantonErrorCapturedandapp.config.errorHandlercould not catch these errors, making it impossible to integrate hydration mismatch reporting with observability tools like Sentry or Datadog.This change routes the "Hydration completed but contains mismatches" error through Vue's
handleError()pipeline:ErrorCodes.HYDRATION_MISMATCHto the error code enum so the error type is properly identifiedlogMismatchError()to accept aparentComponentparameter and callhandleError()instead ofconsole.error()logMismatchError()to pass the currentparentComponentconsole.errorthroughhandleError's defaultlogErrorfallbackErrorobject (not a plain string), providing a proper stack traceExample
Test plan
app.config.errorHandlercatches hydration mismatch erroronErrorCapturedcatches hydration mismatch errorCloses #13154
Summary by CodeRabbit
Bug Fixes
errorHandlerandonErrorCaptured) instead of only logging to console, providing better error visibility and handling control.Tests