sveltejs / sveltejs/kit

`navigating` stays set after an `invalidateAll()` aborts an in-flight `goto()`

Open
#17,114 5 comments 1 reaction 0 assignees View on GitHub

@Nic-Polumeyv is already working on this.

Since Sep 16, 2026.

  • #17118 by @Nic-Polumeyv — open
awaiting submitter
Dominant language
JavaScript
Stars
20.8k
Forks
2.3k
Avg merge
1d 16h
Merged PRs (30d)
156

Description

Describe the bug

invalidate() or invalidateAll() can run while a client-side navigation is still loading. That navigation is then aborted, but navigating is never reset. It keeps holding the aborted navigation until another navigation completes, so anything that shows a loading state from navigating stays in that state.

From packages/kit/src/runtime/client/client.js in 2.70.3:

  • navigate() sets stores.navigating / navigating.current when the navigation starts (:1758).
  • _invalidate() takes a new token (:425, const nav_token = (token = {})).
  • When the navigation's load returns, navigate() sees that the token has changed (:1809). It calls nav.reject(new Error('navigation aborted')) and returns.
  • The only reset of navigating for a navigation happens at the end of one that completes (:2050), and that early return skips it. :1988 is a second early return on the same token check.
Reproduction

A fresh npx sv create --template minimal --types ts app, with @sveltejs/kit 2.70.3 and svelte 5.57.0.

src/routes/+page.ts:

export async function load({ url }) {
  if (url.searchParams.has('slow')) await new Promise((r) => setTimeout(r, 1000));
  return {};
}

src/routes/+page.svelte:

<script lang="ts">
  import { goto, invalidateAll } from '$app/navigation';
  import { navigating } from '$app/state';

  function go() {
    goto('?slow=1').catch(() => {});
    setTimeout(() => invalidateAll(), 100);
  }
  function goPlain() {
    goto('?slow=1').catch(() => {});
  }
</script>

<button id="go" onclick={go}>go</button>
<button id="plain" onclick={goPlain}>go without invalidate</button>
<p id="state">navigating: {navigating.to ? 'yes' : 'no'}</p>

Clicking each button in Chromium (driven by Playwright, reading #state 200 ms after the click and again 3 s later) gives:

button 200 ms after the click 3 s later URL afterwards
go without invalidate (control) navigating: yes navigating: no ?slow=1
go (invalidateAll 100 ms after goto) navigating: yes navigating: yes unchanged

The aborted navigation's URL is never applied, which seems right. The problem is that navigating stays set indefinitely, until some later navigation completes.

We met this in an app where a search box calls goto on a debounce while a form's invalidateAll, after a save, lands inside the search's load. The app's loading overlay reads navigating, so it stayed up for good.

Expected behaviour

navigating goes back to null when a navigation is aborted, just as it does when one completes. Alternatively, whatever superseded the navigation could take the store over.

Workaround

Treat a navigation as over once its complete promise settles, either way:

let ended = $state.raw(null); // raw: the store holds the object itself, and a proxy is never identical to it
$effect(() => {
  const n = $navigating;
  if (n) n.complete.then(() => (ended = n), () => (ended = n));
});
const isNavigating = $derived($navigating !== null && $navigating !== ended);
Related

#13778 (aborted navigations fail silently) is the same abort path, seen from the side of whoever called goto.

System Info

@sveltejs/kit 2.70.3, svelte 5.57.0, the Vite version from the minimal template, Chromium via Playwright, macOS. By reading the code, the same early return exists in 2.59.1, but I have not run it there.

Severity

annoying, but we have a workaround

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/kit/src/runtime/client/client.js, reading navigate() around lines 1758, 1988, 2050 and _invalidate() around line 425. Reproduce the issue with the provided minimal app and Playwright steps, then verify that aborting a navigation after invalidateAll() clears navigating and that the normal navigation control still behaves as expected.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.