#17114·kit

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

Author: HVossi92Created Sep 15, 2026Updated Sep 17, 2026
Labelsawaiting submitter

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:

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

src/routes/+page.svelte:

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:

typescript
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