diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 69fc68c217..086dc4d584 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -213,37 +213,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; - - /** @type {any} */ - let threw = false; + 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) { - threw = true; + 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) { - // Spec says a non-normal completion is break, return, or throw, in which case iterator.return() should be called... - const result = (await track_reactivity_loss(iterator.return()))(); - - // ... but only in case of break or return the result is returned, otherwise error takes precedence - if (!threw) { - // eslint-disable-next-line no-unsafe-finally - return /** @type {TReturn} */ (result.value); - } + // 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) { + return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))()); } } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/_config.js similarity index 87% rename from packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws/_config.js rename to packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/_config.js index 3eec3f4141..9785f639cb 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/_config.js @@ -12,7 +12,7 @@ export default test({ assert.htmlEqual( target.innerHTML, - '

number -> number -> number -> body failed -> ended

' + '

number -> number -> number -> return -> body failed -> ended

' ); assert.deepEqual(normalise_trace_logs(warnings), [ 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: '

pending

', + async test({ assert, target, warnings }) { + await tick(); + + assert.htmlEqual(target.innerHTML, '

number -> number -> next 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/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte similarity index 59% rename from packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws/main.svelte rename to packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte index 43658cc921..ffe2ef93c0 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte @@ -4,15 +4,22 @@ async function get_result() { const logs = []; - async function* iterator() { - yield values[0]; - yield values[1]; - yield values[2]; - throw new Error('body failed'); - } + const iterator = { + index: 0, + async next() { + if (this.index > 1) throw new Error('next failed'); + return { done: false, value: values[this.index++] }; + }, + async return() { + logs.push('return'); + }, + [Symbol.asyncIterator]() { + return this; + } + }; try { - for await (const value of iterator()) { + for await (const value of iterator) { logs.push('number'); // Read reactive state after async iterator await. values.length === value;