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}
+