`navigating` stays set after an `invalidateAll()` aborts an in-flight `goto()`
- 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()setsstores.navigating/navigating.currentwhen 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 callsnav.reject(new Error('navigation aborted'))and returns. - The only reset of
navigatingfor a navigation happens at the end of one that completes (:2050), and that early return skips it.:1988is 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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