Repository: sveltejs/svelte (2025-07-16T21:40:51Z)

Issue:
Testing of rune effects is broken
### Describe the bug

Testing rune effects as described in [Svelte documentation](https://svelte.dev/docs/svelte/testing#Unit-and-integration-testing-using-Vitest-Using-runes-inside-your-test-files) is broken in Svelte > 5.1.11 and is double broken with vitest 3.2.x

Repo has `logger.svelte.test.js` from documentation with latest svelte and vitest versions. Its expectation fails as `flushSync()` seems to be ignored and code in $effect rune is not run.

Downgrading vitest version to 3.1.4 changes test behavior - it fails with `effect_update_depth_exceeded`.

Downgrading Svelte as well to 5.1.11 or older makes test to pass.

### Reproduction

Repo: https://github.com/vits/vitest-svelte-effects



### Logs

```shell
Any version of Svelte 5 with vitest 3.2.x:

AssertionError: expected [] to deeply equal [ +0 ]

- Expected
+ Received

- [
-   0,
- ]
+ []

 ❯ src/logger.svelte.test.js:15:21
     13|   // use flushSync to execute all pending effects synchronously
     14|   flushSync();
     15|   expect(log.value).toEqual([0]);
       |                     ^
     16|
     17|   count = 1;
 ❯ update_reaction node_modules/.pnpm/svelte@5.33.14/node_modules/svelte/src/internal/client/runtime.js:414:56
 ❯ update_effect node_modules/.pnpm/svelte@5.33.14/node_modules/svelte/src/internal/client/runtime.js:580:18
 ❯ create_effect node_modules/.pnpm/svelte@5.33.14/node_modules/svelte/src/internal/client/reactivity/effects.js:118:17
 ❯ Module.effect_root node_modules/.pnpm/svelte@5.33.14/node_modules/svelte/src/internal/client/reactivity/effects.js:231:17
 ❯ src/logger.svelte.test.js:6:15

Svelte > 5.1.1 with vitest 3.1.4:

 ❯  client  src/logger.svelte.test.js (1 test | 1 failed) 9ms
   × Effect 8ms
     → effect_update_depth_exceeded
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
https://svelte.dev/e/effect_update_depth_exceeded
```

### System Info

```shell
System:
    OS: macOS 15.5
    CPU: (10) arm64 Apple M1 Pro
    Memory: 96.94 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.14.0 - ~/.local/share/mise/installs/node/22.14.0/bin/node
    npm: 10.9.2 - ~/.local/share/mise/installs/node/22.14.0/bin/npm
    pnpm: 9.15.7 - ~/Library/pnpm/pnpm
  Browsers:
    Chrome: 137.0.7151.69
    Edge: 137.0.3296.62
    Safari: 18.5
  npmPackages:
    svelte: ^5.33.14 => 5.33.14
```

### Severity

annoyance


Discussion:
Likely related: I'm encountering `vitest` failure from minimum project created by `sv`.

**To reproduce:**
1. Create a minimum project with `npx sv create`. (I also use node adapter and pnpm here.)
2. Update libraries in package.json to latest versions: `"vitest": "3.2.2"`, and install the updates `pnpm i`.
3. Run vitest `pnpm run test:unit`
4. Test fails
```
 ❯ |client| src/routes/page.svelte.test.ts (1 test | 1 failed) 6ms
   × /+page.svelte > should render h1 5ms
     → lifecycle_function_unavailable
`mount(...)` is not available on the server
https://svelte.dev/e/lifecycle_function_unavailable

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  |client| src/routes/page.svelte.test.ts > /+page.svelte > should render h1
Svelte error: lifecycle_function_unavailable
`mount(...)` is not available on the server
https://svelte.dev/e/lifecycle_function_unavailable
 ❯ Module.lifecycle_function_unavailable node_modules/.pnpm/svelte@5.33.14/node_modules/svelte/src/internal/server/errors.js:9:16
      7|  */
      8| export function lifecycle_function_unavailable(name) {
      9|  const error = new Error(`lifecycle_function_unavailable\n\`${name}(..…
       |                ^
     10| 
     11|  error.name = 'Svelte error';
 ❯ Module.mount node_modules/.pnpm/svelte@5.33.14/node_modules/svelte/src/index-server.js:25:4
 ❯ mount node_modules/.pnpm/@testing-library+svelte@5.2.8_svelte@5.33.14_vite@6.3.5_@types+node@22.15.30_jiti@2.4.2_75bbe56ae1d328546c39894cf011ca14/node_modules/@testing-library/svelte/src/core/modern.svelte.js:27:28
 ❯ render node_modules/.pnpm/@testing-library+svelte@5.2.8_svelte@5.33.14_vite@6.3.5_@types+node@22.15.30_jiti@2.4.2_75bbe56ae1d328546c39894cf011ca14/node_modules/@testing-library/svelte/src/pure.js:75:26
 ❯ src/routes/page.svelte.test.ts:8:3
```

Test fails since `vitest@3.2.0`. Test works with `vitest@3.1.4`.
This is related to #14239 so maybe should have added to existing issue. My bad. Hovewer solution to use `environment: jsdom` doesn't seem to work, as my repo is created with latest `sv` and there is environment: jsdom configuration for `client` workspace.

Same issue, `npm run test` fails after upgrading vitest from 3.1.3 to 3.2.2

```
 DEPRECATED  The `workspace` option is deprecated and will be removed in the next major. To hide this warning, rename `workspace` option to `projects`.

 RUN  v3.2.2 /Users/emin/Sources/tmp/sv4

 ✓  server  src/demo.spec.ts (1 test) 1ms
 ❯  client  src/routes/page.svelte.test.ts (1 test | 1 failed) 5ms
   × /+page.svelte > should render h1 5ms
     → lifecycle_function_unavailable
`mount(...)` is not available on the server
https://svelte.dev/e/lifecycle_function_unavailable

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL   client  src/routes/page.svelte.test.ts > /+page.svelte > should render h1
Svelte error: lifecycle_function_unavailable
`mount(...)` is not available on the server
https://svelte.dev/e/lifecycle_function_unavailable
 ❯ Module.lifecycle_function_unavailable node_modules/svelte/src/internal/server/errors.js:9:16
      7|  */
      8| export function lifecycle_function_unavailable(name) {
      9|  const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`);
       |                ^
     10| 
     11|  error.name = 'Svelte error';
 ❯ Module.mount node_modules/svelte/src/index-server.js:25:4
 ❯ mount node_modules/@testing-library/svelte/src/core/modern.svelte.js:27:28
 ❯ render node_modules/@testing-library/svelte/src/pure.js:75:26
 ❯ src/routes/page.svelte.test.ts:8:3

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯


 Test Files  1 failed | 1 passed (2)
      Tests  1 failed | 1 passed (2)
   Start at  16:32:52
   Duration  843ms (transform 191ms, setup 358ms, collect 37ms, tests 6ms, environment 260ms, prepare 79ms)

```
This appears to have been an issue in Vitest -- 3.2.3 fixes the issue for me. If you're still experiencing it after upgrading, feel free to reopen!
I have no permission to reopen this issue. But vitest 3.2.3 only changes problem back to state it was with vitest <= 3.1.4, as described at the end of my reproduction logs. I.e. `logger.svelte.js` test from Svelte documentation now fails with `effect_update_depth_exceeded`. Downgrading Svelte to 5.1.11 makes test pass.

```
❯  client  src/logger.svelte.test.js (1 test | 1 failed) 9ms
   × Effect 9ms
     → effect_update_depth_exceeded
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
https://svelte.dev/e/effect_update_depth_exceeded
```

Updated my reproduction repo to latest Svelte and Vitest.
Will look into it!
@vits You cannot used `array.push` inside an `$effect`.

`push` internally reads the length of the array, therefore when using it you are at the same time reading and writing to the same array.

This is a problem because the reading phase will add the `array.length` to the deps of the `$effect` and the writing phase will increase it causing the same `$effect` to be invalidated.

I think there are only 2 things that you can do safely in the `$effect`, either you `console.log` the `length` and `push` outside of it, or you can re-assign to a different value eg: `log = Array.from({ length: getValue() }).map((_, i) => i);`.

However it's important you don't write and read on the same variable, you can look at `push` as doing something like `num = num + getValue()` or `log = [...log, getValue()]`, in both cases you can expect the infinite loop due to reading and writing on the same variable.

One possible workaround for user specific case is to wrap `push` into `untrack`, this will allow you to push but you also have to tell svelte that you want it to depend on the value returned by `getValue()`.

The complete solution of your specific reactive module is
```js
$effect(() => {
	const v = getValue();
	untrack(() => {
		log.push(v);
	});
});
```

This will ensure that your `$effect` will listen only to changes of your `count` state that is accessed via `getValue` but will ignore changes to `log`.
@raythurnvoid Thanks, your example with `untrack` works with latest Svelte and vitest.

So this is documentation issue, as the code example with `array.push` inside effect is from Svelte testing documentation, https://svelte.dev/docs/svelte/testing#Unit-and-integration-testing-using-Vitest-Using-runes-inside-your-test-files. Should be fixed there.

Ah @vits good catch, the docs should not be doing that. It sounds like there's not actually a bug here after all (after the Vite issue was fixed, at least). 
@vits The docs are different, they do not use a `$state` to store the `log`. This is a fundamental difference because it means the `$effect` in the docs **does not depend** on `log` itself.



Patch:
diff --git a/.changeset/slimy-doors-fetch.md b/.changeset/slimy-doors-fetch.md
new file mode 100644
index 000000000000..8dec24a98dab
--- /dev/null
+++ b/.changeset/slimy-doors-fetch.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: more informative error when effects run in an infinite loop
diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md
index c7d6ec8ac9cd..3b17ef9f9b4e 100644
--- a/documentation/docs/98-reference/.generated/client-errors.md
+++ b/documentation/docs/98-reference/.generated/client-errors.md
@@ -89,9 +89,47 @@ Effect cannot be created inside a `$derived` value that was not itself created i
 ### effect_update_depth_exceeded
 
 ```
-Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
+Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state
 ```
 
+If an effect updates some state that it also depends on, it will re-run, potentially in a loop:
+
+```js
+let count = $state(0);
+
+$effect(() => {
+	// this both reads and writes `count`,
+	// so will run in an infinite loop
+	count += 1;
+});
+```
+
+(Svelte intervenes before this can crash your browser tab.)
+
+The same applies to array mutations, since these both read and write to the array:
+
+```js
+let array = $state([]);
+
+$effect(() => {
+	array.push('hello');
+});
+```
+
+Note that it's fine for an effect to re-run itself as long as it 'settles':
+
+```js
+let array = ['a', 'b', 'c'];
+// ---cut---
+$effect(() => {
+	// this is okay, because sorting an already-sorted array
+	// won't result in a mutation
+	array.sort();
+});
+```
+
+Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
+
 ### flush_sync_in_effect
 
 ```
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index 2ce25dbd53e6..d6af8598812b 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -60,7 +60,45 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
 
 ## effect_update_depth_exceeded
 
-> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
+> Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state
+
+If an effect updates some state that it also depends on, it will re-run, potentially in a loop:
+
+```js
+let count = $state(0);
+
+$effect(() => {
+	// this both reads and writes `count`,
+	// so will run in an infinite loop
+	count += 1;
+});
+```
+
+(Svelte intervenes before this can crash your browser tab.)
+
+The same applies to array mutations, since these both read and write to the array:
+
+```js
+let array = $state([]);
+
+$effect(() => {
+	array.push('hello');
+});
+```
+
+Note that it's fine for an effect to re-run itself as long as it 'settles':
+
+```js
+let array = ['a', 'b', 'c'];
+// ---cut---
+$effect(() => {
+	// this is okay, because sorting an already-sorted array
+	// won't result in a mutation
+	array.sort();
+});
+```
+
+Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
 
 ## flush_sync_in_effect
 
diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js
index 128942ceb293..b7a6a385486e 100644
--- a/packages/svelte/src/internal/client/dev/tracing.js
+++ b/packages/svelte/src/internal/client/dev/tracing.js
@@ -56,8 +56,10 @@ function log_entry(signal, entry) {
 	}
 
 	if (dirty && signal.updated) {
-		// eslint-disable-next-line no-console
-		console.log(signal.updated);
+		for (const updated of signal.updated.values()) {
+			// eslint-disable-next-line no-console
+			console.log(updated.error);
+		}
 	}
 
 	if (entry) {
@@ -120,44 +122,46 @@ export function trace(label, fn) {
 
 /**
  * @param {string} label
+ * @returns {Error & { stack: string } | null}
  */
 export function get_stack(label) {
 	let error = Error();
 	const stack = error.stack;
 
-	if (stack) {
-		const lines = stack.split('\n');
-		const new_lines = ['\n'];
-
-		for (let i = 0; i < lines.length; i++) {
-			const line = lines[i];
-
-			if (line === 'Error') {
-				continue;
-			}
-			if (line.includes('validate_each_keys')) {
-				return null;
-			}
-			if (line.includes('svelte/src/internal')) {
-				continue;
-			}
-			new_lines.push(line);
-		}
+	if (!stack) return null;
 
-		if (new_lines.length === 1) {
+	const lines = stack.split('\n');
+	const new_lines = ['\n'];
+
+	for (let i = 0; i < lines.length; i++) {
+		const line = lines[i];
+
+		if (line === 'Error') {
+			continue;
+		}
+		if (line.includes('validate_each_keys')) {
 			return null;
 		}
+		if (line.includes('svelte/src/internal')) {
+			continue;
+		}
+		new_lines.push(line);
+	}
 
-		define_property(error, 'stack', {
-			value: new_lines.join('\n')
-		});
-
-		define_property(error, 'name', {
-			// 'Error' suffix is required for stack traces to be rendered properly
-			value: `${label}Error`
-		});
+	if (new_lines.length === 1) {
+		return null;
 	}
-	return error;
+
+	define_property(error, 'stack', {
+		value: new_lines.join('\n')
+	});
+
+	define_property(error, 'name', {
+		// 'Error' suffix is required for stack traces to be rendered properly
+		value: `${label}Error`
+	});
+
+	return /** @type {Error & { stack: string }} */ (error);
 }
 
 /**
diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js
index a491dc683d9e..edd66a7400d7 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -214,12 +214,12 @@ export function effect_pending_outside_reaction() {
 }
 
 /**
- * Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
+ * Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state
  * @returns {never}
  */
 export function effect_update_depth_exceeded() {
 	if (DEV) {
-		const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops\nhttps://svelte.dev/e/effect_update_depth_exceeded`);
+		const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state\nhttps://svelte.dev/e/effect_update_depth_exceeded`);
 
 		error.name = 'Svelte error';
 
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 1d08b5c3d82b..cdce971b189a 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -46,9 +46,6 @@ export let current_batch = null;
  */
 export let batch_deriveds = null;
 
-/** @type {Effect[]} Stack of effects, dev only */
-export let dev_effect_stack = [];
-
 /** @type {Set<() => void>} */
 export let effect_pending_updates = new Set();
 
@@ -345,6 +342,28 @@ export class Batch {
 
 			while (queued_root_effects.length > 0) {
 				if (flush_count++ > 1000) {
+					if (DEV) {
+						var updates = new Map();
+
+						for (const source of this.#current.keys()) {
+							for (const [stack, update] of source.updated ?? []) {
+								var entry = updates.get(stack);
+
+								if (!entry) {
+									entry = { error: update.error, count: 0 };
+									updates.set(stack, entry);
+								}
+
+								entry.count += update.count;
+							}
+						}
+
+						for (const update of updates.values()) {
+							// eslint-disable-next-line no-console
+							console.error(update.error);
+						}
+					}
+
 					infinite_loop_guard();
 				}
 
@@ -356,9 +375,6 @@ export class Batch {
 			set_is_updating_effect(was_updating_effect);
 
 			last_scheduled_effect = null;
-			if (DEV) {
-				dev_effect_stack = [];
-			}
 		}
 	}
 
@@ -471,10 +487,6 @@ export function flushSync(fn) {
 			// we need to reset it here as well in case the first time there's 0 queued root effects
 			last_scheduled_effect = null;
 
-			if (DEV) {
-				dev_effect_stack = [];
-			}
-
 			return /** @type {T} */ (result);
 		}
 
@@ -482,45 +494,18 @@ export function flushSync(fn) {
 	}
 }
 
-function log_effect_stack() {
-	// eslint-disable-next-line no-console
-	console.error(
-		'Last ten effects were: ',
-		dev_effect_stack.slice(-10).map((d) => d.fn)
-	);
-	dev_effect_stack = [];
-}
-
 function infinite_loop_guard() {
 	try {
 		e.effect_update_depth_exceeded();
 	} catch (error) {
 		if (DEV) {
-			// stack is garbage, ignore. Instead add a console.error message.
-			define_property(error, 'stack', {
-				value: ''
-			});
-		}
-		// Try and handle the error so it can be caught at a boundary, that's
-		// if there's an effect available from when it was last scheduled
-		if (last_scheduled_effect !== null) {
-			if (DEV) {
-				try {
-					invoke_error_boundary(error, last_scheduled_effect);
-				} catch (e) {
-					// Only log the effect stack if the error is re-thrown
-					log_effect_stack();
-					throw e;
-				}
-			} else {
-				invoke_error_boundary(error, last_scheduled_effect);
-			}
-		} else {
-			if (DEV) {
-				log_effect_stack();
-			}
-			throw error;
+			// stack contains no useful information, replace it
+			define_property(error, 'stack', { value: '' });
 		}
+
+		// Best effort: invoke the boundary nearest the most recent
+		// effect and hope that it's relevant to the infinite loop
+		invoke_error_boundary(error, last_scheduled_effect);
 	}
 }
 
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index bd55b9d935f4..9b534d2d7190 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -182,8 +182,22 @@ export function internal_set(source, value) {
 		const batch = Batch.ensure();
 		batch.capture(source, old_value);
 
-		if (DEV && tracing_mode_flag) {
-			source.updated = get_stack('UpdatedAt');
+		if (DEV) {
+			if (tracing_mode_flag || active_effect !== null) {
+				const error = get_stack('UpdatedAt');
+
+				if (error !== null) {
+					source.updated ??= new Map();
+					let entry = source.updated.get(error.stack);
+
+					if (!entry) {
+						entry = { error, count: 0 };
+						source.updated.set(error.stack, entry);
+					}
+
+					entry.count++;
+				}
+			}
 
 			if (active_effect !== null) {
 				source.set_during_effect = true;
diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts
index 90f922df686b..72187e84a720 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -29,8 +29,8 @@ export interface Value<V = unknown> extends Signal {
 	label?: string;
 	/** An error with a stack trace showing when the source was created */
 	created?: Error | null;
-	/** An error with a stack trace showing when the source was last updated */
-	updated?: Error | null;
+	/** An map of errors with stack traces showing when the source was updated, keyed by the stack trace */
+	updated?: Map<string, { error: Error; count: number }> | null;
 	/**
 	 * Whether or not the source was set while running an effect — if so, we need to
 	 * increment the write version so that it shows up as dirty when the effect re-runs
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 6c4d92bbadf5..306b9b9dd9a1 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -42,13 +42,7 @@ import {
 	set_dev_stack
 } from './context.js';
 import * as w from './warnings.js';
-import {
-	Batch,
-	batch_deriveds,
-	dev_effect_stack,
-	flushSync,
-	schedule_effect
-} from './reactivity/batch.js';
+import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js';
 import { handle_error } from './error-handling.js';
 import { UNINITIALIZED } from '../../constants.js';
 
@@ -491,10 +485,6 @@ export function update_effect(effect) {
 				}
 			}
 		}
-
-		if (DEV) {
-			dev_effect_stack.push(effect);
-		}
 	} finally {
 		is_updating_effect = was_updating_effect;
 		active_effect = previous_effect;


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js
new file mode 100644
index 000000000000..400495050cfd
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js
@@ -0,0 +1,21 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	mode: ['client', 'hydrate'],
+
+	compileOptions: {
+		dev: true
+	},
+
+	test({ assert, errors }) {
+		const [button] = document.querySelectorAll('button');
+
+		try {
+			flushSync(() => button.click());
+		} catch (e) {
+			assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be UpdatedAtError
+			assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded'));
+		}
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte
new file mode 100644
index 000000000000..ddb91a90ad22
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte
@@ -0,0 +1,12 @@
+<script>
+	let condition = $state(false);
+	let count = $state(0);
+
+	$effect(() => {
+		if (condition) {
+			count++;
+		}
+	});
+</script>
+
+<button onclick={() => condition = !condition}>toggle</button>
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js
index ccff614ade0d..e3a3b0c7d752 100644
--- a/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js
@@ -2,12 +2,14 @@ import { flushSync } from 'svelte';
 import { test } from '../../test';
 
 export default test({
-	test({ assert, target }) {
+	test({ assert, target, errors }) {
 		let btn = target.querySelector('button');
 
 		btn?.click();
 		flushSync();
 
+		assert.equal(errors.length, 1);
+
 		assert.htmlEqual(target.innerHTML, `<div class="error">An error occurred!</div>`);
 	}
 });