Skip to content

fix: leave stale promises to wait for a later resolution, instead of rejecting#18180

Merged
dummdidumm merged 15 commits into
mainfrom
differentiate-stale-reactions
May 7, 2026
Merged

fix: leave stale promises to wait for a later resolution, instead of rejecting#18180
dummdidumm merged 15 commits into
mainfrom
differentiate-stale-reactions

Conversation

@Rich-Harris

Copy link
Copy Markdown
Member

This incorporates some of the fixes and insights from #18177, but gets rid of the skip logic.

Instead, we differentiate between stale and obsolete promises. A promise is stale if it has been overtaken by a subsequent update, and was rejected with STALE_REACTION:

async function search(query: string) {
  return fetch(`/search?q=${query}`, { signal: getAbortSignal() }).then((r) => r.json());
}

In this case, if we start typing pot, and then finish typing potato, the first promise will eventually resolve with the results for /search?q=potato, instead of the batch entering a weird limbo/zombie state.

A promise is obsolete if it belongs to a now-destroyed effect, meaning that toggling show doesn't result in an accumulation of never-resolving batches:

{#if show}
  {await neverResolves()}
{/if}

Before submitting the PR, please make sure you do the following

  • 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
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

@changeset-bot

changeset-bot Bot commented May 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f66366

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot

Copy link
Copy Markdown

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Playground

pnpm add https://pkg.pr.new/svelte@18180

@dummdidumm

Copy link
Copy Markdown
Member

Will investigate tomorrow but I'm not sure if this just shifts the buggy behavior away from stale reactions towards obsolete ones (i.e. that you can construct test cases which are similar to the ones with getAbortSignal but not using that, instead destroying if blocks, making prior deriveds obsolete, etc).
Also wondering what happens if a derived with getAbortSignal is inside an if block that is destroyed - does that fire the abort signal and does that mean a batch could become a zombie batch that way?

@Rich-Harris

Copy link
Copy Markdown
Member Author

On a mechanical level I feel good about the fix, because the problem we had was one of asymmetry — it was possible to increment a batch but not (properly) decrement it. That was always trivial to fix by getting rid of the skip argument but it resulted in tearing; this approach solves that AFAICT.

On a conceptual level I feel good about it too — when you use getAbortSignal(), you're explicitly saying 'I don't care about this value, I want to wait for the next one', and the abort only happens when there is a next value being produced. The way to express that is to leave the earlier promise in a pending state. It's certainly possible that the implementation isn't quite right (would definitely welcome other test cases) but I think the overall direction is the correct one.

Also wondering what happens if a derived with getAbortSignal is inside an if block that is destroyed - does that fire the abort signal and does that mean a batch could become a zombie batch that way?

When the abort signal aborts, nothing happens, so 'if a derived with getAbortSignal is inside an if block that is destroyed' is the same as 'if a derived is inside an if block that is destroyed'. I don't see a way for zombie batches to occur (except in the case of a never-resolving promise that is never superseded, which we can't solve but also don't need to)

Comment thread packages/svelte/src/internal/client/reactivity/deriveds.js
@dummdidumm

dummdidumm commented May 6, 2026

Copy link
Copy Markdown
Member

Here's an example where you see out-of-sync values (1 + 0 = 0) which you don't see on main currently. Quick debugging let to internal_set thinking "oh this source already has this value (1) so I'm not gonna set it again" but that's wrong in the context of that batch, because for that batch the value is 0 still. But even if we were to fix that it would still be wrong, because then it would show (1 + 0 = 2). The problem is that the first batch just isn't finished yet, and so we can't flush it. We can only do that once we know that the things that superseed it have resolved.

Update: may have found a solution - defer the resolve of earlier batches until the current batch commits. That way there's no tearing, and the earlier batch still has time to resolve itself before that. Update: Did that in #18181

@Rich-Harris

Copy link
Copy Markdown
Member Author

Gah, merging main caused one test to fail. Not sure yet which recent PR is responsible

Rich-Harris and others added 2 commits May 6, 2026 18:26
)

builds on top of #18180

Tweaks the timing when we resolve earlier/outdated batches. Instead of
doing that right away we only do it once the current batch is committed.
That way
- the old batch will not render right away in case this was the last
pending value, while the current batch is still pending, causing tearing
(because we partly render values of the current batch in the old batch
already)
- the old batch still has a chance to resolve itself in the meantime if
the current batch is still pending in other areas

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
@dummdidumm dummdidumm merged commit 908c9d0 into main May 7, 2026
20 checks passed
@dummdidumm dummdidumm deleted the differentiate-stale-reactions branch May 7, 2026 08:30
@github-actions github-actions Bot mentioned this pull request May 6, 2026
Rich-Harris pushed a commit that referenced this pull request May 14, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## svelte@5.55.6

### Patch Changes

- fix: leave stale promises to wait for a later resolution, instead of
rejecting ([#18180](#18180))

- fix: keep dependencies of `$state.eager/pending`
([#18218](#18218))

- fix: reapply context after transforming error during SSR
([#18099](#18099))

- fix: don't rebase just-created batches
([#18117](#18117))

- chore: allow `null` for `pending` in typings
([#18201](#18201))

- fix: flush eager effects in production
([#18107](#18107))

- fix: rethrow error of failed iterable after calling `return()`
([#18169](#18169))

- fix: account for proxified instance when updating `bind:this`
([#18147](#18147))

- fix: ensure scheduled batch is flushed if not obsolete
([#18131](#18131))

- fix: resolve stale deriveds with latest value
([#18167](#18167))

- chore: remove unnecessary `increment_pending` calls
([#18183](#18183))

- fix: correctly compile component member expressions for SSR
([#18192](#18192))

- fix: reset `source.updated` stack traces after `flush`
([#18196](#18196))

- fix: replacing async 'blocking' strategy with 'merging'
([#18205](#18205))

- fix: allow `@debug` tags to reference awaited variables
([#18138](#18138))

- fix: re-run fallback props if dependencies update
([#18146](#18146))

- fix: abort running obsolete async branches
([#18118](#18118))

- fix: ignore comments when reading CSS values
([#18153](#18153))

- fix: wrap `Promise.all` in `save` during SSR
([#18178](#18178))

- fix: ignore false-positive errors of `$inspect` dependencies
([#18106](#18106))

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants