diff --git a/.changeset/big-webs-sing.md b/.changeset/big-webs-sing.md
new file mode 100644
index 0000000000..946a41d881
--- /dev/null
+++ b/.changeset/big-webs-sing.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: leave stale promises to wait for a later resolution, instead of rejecting
diff --git a/.changeset/cruel-boxes-serve.md b/.changeset/cruel-boxes-serve.md
new file mode 100644
index 0000000000..592cec4d01
--- /dev/null
+++ b/.changeset/cruel-boxes-serve.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: reapply context after transforming error during SSR
diff --git a/.changeset/fresh-stars-grin.md b/.changeset/fresh-stars-grin.md
new file mode 100644
index 0000000000..3d56792d1e
--- /dev/null
+++ b/.changeset/fresh-stars-grin.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: rethrow error of failed iterable after calling `return()`
diff --git a/.changeset/modern-tables-fetch.md b/.changeset/modern-tables-fetch.md
new file mode 100644
index 0000000000..89543910fa
--- /dev/null
+++ b/.changeset/modern-tables-fetch.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: resolve stale deriveds with latest value
diff --git a/.changeset/public-mammals-float.md b/.changeset/public-mammals-float.md
new file mode 100644
index 0000000000..d890c9e070
--- /dev/null
+++ b/.changeset/public-mammals-float.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+chore: remove unnecessary `increment_pending` calls
diff --git a/.changeset/stupid-baboons-fall.md b/.changeset/stupid-baboons-fall.md
new file mode 100644
index 0000000000..66895ad015
--- /dev/null
+++ b/.changeset/stupid-baboons-fall.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: abort running obsolete async branches
diff --git a/.changeset/tough-knives-smell.md b/.changeset/tough-knives-smell.md
new file mode 100644
index 0000000000..7687188c1a
--- /dev/null
+++ b/.changeset/tough-knives-smell.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: wrap `Promise.all` in `save` during SSR
diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md
index c1bc69c1c2..0ad82a19b5 100644
--- a/documentation/docs/07-misc/02-testing.md
+++ b/documentation/docs/07-misc/02-testing.md
@@ -38,6 +38,21 @@ You can now write unit tests for code inside your `.js/.ts` files:
```js
/// file: multiplier.svelte.test.js
+// @filename: multiplier.svelte.ts
+export function multiplier(initial: number, k: number) {
+ let count = $state(initial);
+
+ return {
+ get value() {
+ return count * k;
+ },
+ set: (c: number) => {
+ count = c;
+ }
+ };
+}
+// @filename: multiplier.svelte.test.js
+// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
@@ -80,6 +95,16 @@ Since Vitest processes your test files the same way as your source files, you ca
```js
/// file: multiplier.svelte.test.js
+// @filename: multiplier.svelte.ts
+export function multiplier(getCount: () => number, k: number) {
+ return {
+ get value() {
+ return getCount() * k;
+ }
+ };
+}
+// @filename: multiplier.svelte.test.js
+// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
@@ -115,6 +140,10 @@ If the code being tested uses effects, you need to wrap the test inside `$effect
```js
/// file: logger.svelte.test.js
+// @filename: logger.svelte.ts
+export function logger(fn: () => void) {}
+// @filename: logger.svelte.test.js
+// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { logger } from './logger.svelte.js';
@@ -213,7 +242,7 @@ test('Component', () => {
expect(document.body.innerHTML).toBe('');
// Click the button, then flush the changes so you can synchronously write expectations
- document.body.querySelector('button').click();
+ document.body.querySelector('button')?.click();
flushSync();
expect(document.body.innerHTML).toBe('');
@@ -226,6 +255,7 @@ test('Component', () => {
While the process is very straightforward, it is also low level and somewhat brittle, as the precise structure of your component may change frequently. Tools like [@testing-library/svelte](https://testing-library.com/docs/svelte-testing-library/intro/) can help streamline your tests. The above test could be rewritten like this:
```js
+// @errors: 2339
/// file: component.test.js
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
@@ -270,9 +300,9 @@ You can create stories for component variations and test interactions with the [
}
});
-
+
-
+
{
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
index a87642bc4c..9b3ac3ad78 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
@@ -12,7 +12,7 @@ import {
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
-import { has_await_expression } from '../../../../../utils/ast.js';
+import { has_await_expression, save } from '../../../../../utils/ast.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
@@ -360,7 +360,7 @@ export class PromiseOptimiser {
return b.const(
b.array_pattern(this.expressions.map((_, i) => b.id(`$$${i}`))),
- b.await(b.call('Promise.all', promises))
+ save(b.call('Promise.all', promises))
);
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js
index 43af3d8dd3..170529a6b9 100644
--- a/packages/svelte/src/internal/client/dom/blocks/async.js
+++ b/packages/svelte/src/internal/client/dom/blocks/async.js
@@ -1,5 +1,5 @@
/** @import { Blocker, TemplateNode, Value } from '#client' */
-import { flatten, increment_pending } from '../../reactivity/async.js';
+import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
@@ -42,8 +42,6 @@ export function async(node, blockers = [], expressions = [], fn) {
return;
}
- const decrement_pending = increment_pending();
-
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
set_hydrate_node(end);
@@ -64,8 +62,6 @@ export function async(node, blockers = [], expressions = [], fn) {
if (was_hydrating) {
set_hydrating(false);
}
-
- decrement_pending();
}
});
}
diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js
index 549141dbfb..ac57709496 100644
--- a/packages/svelte/src/internal/client/reactivity/async.js
+++ b/packages/svelte/src/internal/client/reactivity/async.js
@@ -55,6 +55,10 @@ export function flatten(blockers, sync, async, fn) {
/** @param {Value[]} values */
function finish(values) {
+ if ((parent.f & DESTROYED) !== 0) {
+ return;
+ }
+
var batch = get_latest_async_batch(values);
if (batch) {
restore(false);
@@ -67,28 +71,29 @@ export function flatten(blockers, sync, async, fn) {
try {
fn(values);
} catch (error) {
- if ((parent.f & DESTROYED) === 0) {
- invoke_error_boundary(error, parent);
- }
+ invoke_error_boundary(error, parent);
}
unset_context();
}
+ var decrement_pending = increment_pending();
+
// Fast path: blockers but no async expressions
if (async.length === 0) {
- /** @type {Promise} */ (blocker_promise).then(() => finish(sync.map(d)));
+ /** @type {Promise} */ (blocker_promise)
+ .then(() => finish(sync.map(d)))
+ .finally(decrement_pending);
+
return;
}
- var decrement_pending = increment_pending();
-
// Full path: has async expressions
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => finish([...sync.map(d), ...result]))
.catch((error) => invoke_error_boundary(error, parent))
- .finally(() => decrement_pending());
+ .finally(decrement_pending);
}
if (blocker_promise) {
@@ -238,22 +243,35 @@ export async function* for_await_track_reactivity_loss(iterable) {
throw new TypeError('value is not async iterable');
}
- /** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */
- let normal_completion = false;
+ // eslint-disable-next-line no-useless-assignment
+ let invoke_return = true;
+
try {
while (true) {
const { done, value } = (await track_reactivity_loss(iterator.next()))();
if (done) {
- normal_completion = true;
+ invoke_return = false;
break;
}
var prev = reactivity_loss_tracker;
- yield value;
+ try {
+ yield value;
+ } catch (e) {
+ set_reactivity_loss_tracker(prev);
+ // If the yield throws, we need to call `return` but not return its value, instead rethrow
+ if (iterator.return !== undefined) {
+ (await track_reactivity_loss(iterator.return()))();
+ }
+ throw e;
+ }
set_reactivity_loss_tracker(prev);
}
+ } catch (error) {
+ invoke_return = false;
+ throw error;
} finally {
- // If the iterator had an abrupt completion and `return` is defined on the iterator, call it and return the value
- if (!normal_completion && iterator.return !== undefined) {
+ // If the iterator had an abrupt completion (break) and `return` is defined on the iterator, call it and return the value
+ if (invoke_return && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
@@ -335,7 +353,7 @@ export function run(thunks) {
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
- .finally(() => decrement_pending());
+ .finally(decrement_pending);
return blockers;
}
@@ -359,8 +377,8 @@ export function increment_pending() {
boundary.update_pending_count(1, batch);
batch.increment(blocking, effect);
- return (skip = false) => {
+ return () => {
boundary.update_pending_count(-1, batch);
- batch.decrement(blocking, effect, skip);
+ batch.decrement(blocking, effect);
};
}
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 4239cda04b..a106806721 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -92,6 +92,9 @@ let uid = 1;
export class Batch {
id = uid++;
+ /** True as soon as `#process()` was called */
+ #started = false;
+
/**
* The current values of any signals that are updated in this batch.
* Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment)
@@ -107,6 +110,13 @@ export class Batch {
*/
previous = new Map();
+ /**
+ * Async effects which this batch doesn't take into account anymore when calculating blockers,
+ * as it has a value for it already.
+ * @type {Set}
+ */
+ unblocked = new Set();
+
/**
* When the batch is committed (and the DOM is updated), we need to remove old branches
* and append new ones by calling the functions added inside (if/each/key/etc) blocks
@@ -127,10 +137,9 @@ export class Batch {
#fork_commit_callbacks = new Set();
/**
- * Async effects that are currently in flight
- * @type {Map}
+ * The number of async effects that are currently in flight
*/
- #pending = new Map();
+ #pending = 0;
/**
* Async effects that are currently in flight, _not_ inside a pending boundary
@@ -198,6 +207,8 @@ export class Batch {
#is_blocked() {
for (const batch of this.#blockers) {
for (const effect of batch.#blocking_pending.keys()) {
+ if (this.unblocked.has(effect)) continue;
+
var skipped = false;
var e = effect;
@@ -255,6 +266,8 @@ export class Batch {
}
#process() {
+ this.#started = true;
+
if (flush_count++ > 1000) {
batches.delete(this);
infinite_loop_guard();
@@ -322,7 +335,7 @@ export class Batch {
reset_branch(e, t);
}
} else {
- if (this.#pending.size === 0) {
+ if (this.#pending === 0) {
batches.delete(this);
}
@@ -342,6 +355,8 @@ export class Batch {
this.#deferred?.resolve();
}
+ var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
+
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
// else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
@@ -350,8 +365,6 @@ export class Batch {
this.#commit();
}
- var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
-
// Edge case: During traversal new branches might create effects that run immediately and set state,
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches.
@@ -537,6 +550,8 @@ export class Batch {
sources.push(source);
}
+ if (!batch.#started) continue;
+
// Re-run async/block effects that depend on distinct values changed in both batches
var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
@@ -630,8 +645,7 @@ export class Batch {
* @param {Effect} effect
*/
increment(blocking, effect) {
- let pending_count = this.#pending.get(effect) ?? 0;
- this.#pending.set(effect, pending_count + 1);
+ this.#pending += 1;
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@@ -642,16 +656,9 @@ export class Batch {
/**
* @param {boolean} blocking
* @param {Effect} effect
- * @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
*/
- decrement(blocking, effect, skip) {
- let pending_count = this.#pending.get(effect) ?? 0;
-
- if (pending_count === 1) {
- this.#pending.delete(effect);
- } else {
- this.#pending.set(effect, pending_count - 1);
- }
+ decrement(blocking, effect) {
+ this.#pending -= 1;
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@@ -663,12 +670,15 @@ export class Batch {
}
}
- if (this.#decrement_queued || skip) return;
+ if (this.#decrement_queued) return;
this.#decrement_queued = true;
queue_micro_task(() => {
this.#decrement_queued = false;
- this.flush();
+
+ if (batches.has(this)) {
+ this.flush();
+ }
});
}
@@ -722,7 +732,7 @@ export class Batch {
if (!is_flushing_sync) {
queue_micro_task(() => {
- if (!batches.has(batch) || batch.#pending.size > 0) {
+ if (batch.#started) {
// a flushSync happened in the meantime
return;
}
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index b4ee0ce8a0..bc6483d61c 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -100,6 +100,8 @@ export function derived(fn) {
return signal;
}
+const OBSOLETE = {};
+
/**
* @template V
* @param {() => V | Promise} fn
@@ -118,7 +120,7 @@ export function async_derived(fn, label, location) {
var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
- if (DEV) signal.label = label;
+ if (DEV) signal.label = label ?? fn.toString();
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
@@ -141,7 +143,13 @@ export function async_derived(fn, label, location) {
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
- Promise.resolve(fn()).then(d.resolve, d.reject).finally(unset_context);
+ Promise.resolve(fn())
+ .then(d.resolve, (e) => {
+ // if the promise was rejected by the user, via `getAbortSignal`, then
+ // wait for a subsequent resolution instead of flushing the batch
+ if (e !== STALE_REACTION) d.reject(e);
+ })
+ .finally(unset_context);
} catch (error) {
d.reject(error);
unset_context();
@@ -180,15 +188,13 @@ export function async_derived(fn, label, location) {
}
if (/** @type {Boundary} */ (parent.b).is_rendered()) {
- deferreds.get(batch)?.reject(STALE_REACTION);
- deferreds.delete(batch); // delete to ensure correct order in Map iteration below
+ deferreds.get(batch)?.reject(OBSOLETE);
} else {
// While the boundary is still showing pending, a new run supersedes all older in-flight runs
// for this async expression. Cancel eagerly so resolution cannot commit stale values.
for (const d of deferreds.values()) {
- d.reject(STALE_REACTION);
+ d.reject(OBSOLETE);
}
- deferreds.clear();
}
deferreds.set(batch, d);
@@ -203,16 +209,10 @@ export function async_derived(fn, label, location) {
reactivity_loss_tracker = null;
}
- if (decrement_pending) {
- // don't trigger an update if we're only here because
- // the promise was superseded before it could resolve
- var skip = error === STALE_REACTION;
- decrement_pending(skip);
- }
+ decrement_pending?.();
+ deferreds.delete(batch);
- if (error === STALE_REACTION || (effect.f & DESTROYED) !== 0) {
- return;
- }
+ if (error === OBSOLETE) return;
batch.activate();
/** @type {Source & { async_batch?: Batch }} */ (signal).async_batch = batch;
@@ -231,16 +231,21 @@ export function async_derived(fn, label, location) {
// All prior async derived runs are now stale
for (const [b, d] of deferreds) {
- deferreds.delete(b);
- if (b === batch) break;
- d.reject(STALE_REACTION);
+ if (b.id < batch.id) {
+ // Don't delete + resolve directly, instead only do that once
+ // the current batch commits. This way we avoid tearing when
+ // `b` is rendering through the early resolve while `batch` is
+ // still pending.
+ batch.unblocked.add(effect);
+ batch.oncommit(() => d.resolve(value));
+ }
}
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
setTimeout(() => {
- if (recent_async_deriveds.has(signal)) {
+ if (recent_async_deriveds.has(signal) && (effect.f & DESTROYED) === 0) {
w.await_waterfall(/** @type {string} */ (signal.label), location);
recent_async_deriveds.delete(signal);
}
@@ -256,7 +261,7 @@ export function async_derived(fn, label, location) {
teardown(() => {
for (const d of deferreds.values()) {
- d.reject(STALE_REACTION);
+ d.reject(OBSOLETE);
}
});
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 0fad074e6f..5bdba037b1 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -43,7 +43,7 @@ import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, collected_effects, current_batch } from './batch.js';
-import { flatten, increment_pending } from './async.js';
+import { flatten } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js';
@@ -396,16 +396,8 @@ export function template_effect(fn, sync = [], async = [], blockers = []) {
* @param {Blocker[]} blockers
*/
export function deferred_template_effect(fn, sync = [], async = [], blockers = []) {
- if (async.length > 0 || blockers.length > 0) {
- var decrement_pending = increment_pending();
- }
-
flatten(blockers, sync, async, (values) => {
create_effect(EFFECT, () => fn(...values.map(get)));
-
- if (decrement_pending) {
- decrement_pending();
- }
});
}
diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js
index d2ab35a1f2..35aac64721 100644
--- a/packages/svelte/src/internal/server/renderer.js
+++ b/packages/svelte/src/internal/server/renderer.js
@@ -715,7 +715,12 @@ export class Renderer {
const { context, failed, transformError } = item.#boundary;
set_ssr_context(context);
- let transformed = await transformError(error);
+
+ let promise = transformError(error);
+ set_ssr_context(null);
+
+ let transformed = await promise;
+ set_ssr_context(context);
// Render the failed snippet instead of the partial children content
const failed_renderer = new Renderer(item.global, item);
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index 454ae2f766..6f30fb5d98 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -60,6 +60,8 @@ export interface RuntimeTest = Record void;
after_test?: () => void;
+ /** If true, flushSync() will not be called before invoking test() */
+ skip_initial_flushSync?: boolean;
test?: (args: {
variant: 'dom' | 'hydrate';
assert: Assert;
@@ -505,7 +507,7 @@ async function run_test_variant(
try {
if (config.test) {
- flushSync();
+ if (!config.skip_initial_flushSync) flushSync();
if (variant === 'hydrate' && cwd.includes('async-')) {
// wait for pending boundaries to render
@@ -543,7 +545,7 @@ async function run_test_variant(
}
} finally {
if (runes) {
- unmount(instance);
+ await unmount(instance);
} else {
instance.$destroy();
}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-order/_config.js b/packages/svelte/tests/runtime-runes/samples/async-batch-order/_config.js
new file mode 100644
index 0000000000..53cceb9d54
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-batch-order/_config.js
@@ -0,0 +1,30 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ await tick();
+ const [increment, shift, middle] = target.querySelectorAll('button');
+ const [div] = target.querySelectorAll('div');
+
+ increment.click();
+ await tick();
+ increment.click();
+ await tick();
+ increment.click();
+ await tick();
+ middle.click(); // resolve the second increment which will make the if block go away and the first batch discarded
+ await tick();
+ assert.htmlEqual(div.innerHTML, '2 2');
+
+ shift.click();
+ await tick();
+ shift.click();
+ await tick();
+ shift.click();
+ await tick();
+ shift.click();
+ await tick();
+ assert.htmlEqual(div.innerHTML, '3 3');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-order/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-batch-order/main.svelte
new file mode 100644
index 0000000000..0289380d78
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-batch-order/main.svelte
@@ -0,0 +1,21 @@
+
+
+
number -> number -> number -> return -> body failed -> ended
'
+ );
+
+ assert.deepEqual(normalise_trace_logs(warnings), [
+ {
+ log: 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`'
+ }
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/main.svelte
new file mode 100644
index 0000000000..da7c48642c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/main.svelte
@@ -0,0 +1,45 @@
+
+
+
+
{await get_result()}
+
+ {#snippet pending()}
+
pending
+ {/snippet}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/_config.js
new file mode 100644
index 0000000000..9e8a2d8def
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/_config.js
@@ -0,0 +1,21 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+import { normalise_trace_logs } from '../../../helpers.js';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ html: '
');
+
+ assert.deepEqual(normalise_trace_logs(warnings), [
+ {
+ log: 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`'
+ }
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte
new file mode 100644
index 0000000000..ffe2ef93c0
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte
@@ -0,0 +1,43 @@
+
+
+
+