From 325620ba63eeff13c9f36e42806a545aaa7ee242 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Thu, 24 Sep 2026 00:37:04 +0800 Subject: [PATCH] fix: bypass error boundaries for hydration recovery (#18841) Fixes #18840 This PR rethrows the hydration error rather than catching it at the boundaries so that the svelte hydration warning can surface ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .changeset/tidy-pandas-hydrate.md | 5 +++++ .../svelte/src/internal/client/dom/blocks/boundary.js | 10 +++++++++- packages/svelte/src/internal/client/error-handling.js | 6 +++++- .../samples/boundary-mismatch-recovery/_config.js | 5 +++++ .../samples/boundary-mismatch-recovery/_expected.html | 1 + .../samples/boundary-mismatch-recovery/main.svelte | 7 +++++++ 6 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-pandas-hydrate.md create mode 100644 packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_config.js create mode 100644 packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/main.svelte diff --git a/.changeset/tidy-pandas-hydrate.md b/.changeset/tidy-pandas-hydrate.md new file mode 100644 index 0000000000..944797a760 --- /dev/null +++ b/.changeset/tidy-pandas-hydrate.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent hydration mismatch recovery from being intercepted by error boundaries diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index fd6bbdf5b4..979d3e50b4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,10 @@ /** @import { Effect, Source, TemplateNode, } from '#client' */ import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants'; -import { HYDRATION_START_ELSE, HYDRATION_START_FAILED } from '../../../../constants.js'; +import { + HYDRATION_ERROR, + HYDRATION_START_ELSE, + HYDRATION_START_FAILED +} from '../../../../constants.js'; import { component_context, set_component_context } from '../../context.js'; import { invoke_error_boundary } from '../../error-handling.js'; import { @@ -445,6 +449,10 @@ export class Boundary { /** @param {unknown} error */ error(error) { + if (error === HYDRATION_ERROR) { + throw error; + } + // If we have nothing to capture the error, or if we hit an error while // rendering the fallback, re-throw for another boundary to handle if (!this.#props.onerror && !this.#props.failed) { diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index a46281d36c..7c69f05dcc 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -1,7 +1,7 @@ /** @import { Derived, Effect } from '#client' */ /** @import { Boundary } from './dom/blocks/boundary.js' */ import { DEV } from 'esm-env'; -import { FILENAME } from '../../constants.js'; +import { FILENAME, HYDRATION_ERROR } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; import { ERROR_VALUE, @@ -52,6 +52,10 @@ export function handle_error(error) { * @param {Effect | null} effect */ export function invoke_error_boundary(error, effect) { + if (error === HYDRATION_ERROR) { + throw error; + } + if (effect !== null && (effect.f & DESTROYED) !== 0) { return; } diff --git a/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_config.js b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_config.js new file mode 100644 index 0000000000..cf22ff2c85 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_expected.html b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_expected.html new file mode 100644 index 0000000000..8286c59709 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_expected.html @@ -0,0 +1 @@ +

Valid HTML fragment

diff --git a/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/main.svelte b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/main.svelte new file mode 100644 index 0000000000..8b9cc0770e --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/main.svelte @@ -0,0 +1,7 @@ + +

{@html '

Valid HTML fragment

'}

+ + {#snippet failed()} +

boundary fallback

+ {/snippet} +