Which project does this relate to? Router
Describe the bug
A failed background refresh replaces a working page with an error page.
A route loads successfully and displays its content. Later, the router re-runs its loader in the background to refresh stale data. If that loader throws, the router changes the match’s status to 'error' and replaces the page with its errorComponent.
The previous loaderData is still available on the match. The router could keep showing the existing content, but instead the user loses the page they were viewing because a background refresh failed.
I’d expect the existing page to stay visible, with the app deciding whether to notify the user or offer a retry. Replacing it with an error page seems appropriate when the initial load fails, but looks like a bug when a background refresh fails.
Possible cause
In load-client, runBackground() appears to handle a loader error through the same install() path used for a blocking load. It then calls publishMatches(router, projected[1]), making the error state visible and replacing the page.
That publish step is skipped if another transaction has superseded the refresh, or for control outcomes such as redirects and cancellation. There doesn’t appear to be a separate check for a failed background loader that would preserve the successfully loaded page.
Complete minimal reproducer
https://github.com/TylerRick/tanstack-router-background-reload-error-repro
Steps to Reproduce the Bug
pnpm install && pnpm dev, open http://localhost:5596 — the page renders PAGE: FIRST from a successful loader.
- Press fail the next load, then router.invalidate() (background). It sets a flag that makes the loader throw a plain
Error, then calls router.invalidate() with no sync. The router has defaultStaleReloadMode: 'background' and defaultStaleTime: 0.
- The harness waits for the loader to re-run and for fetching to stop (no sleeps), then prints the match snapshot before and after, plus the rendered heading:
before: {"routeId":"/","status":"success","invalid":false,"hasLoaderData":true,"isFetching":false}
after: {"routeId":"/","status":"error","invalid":true,"hasLoaderData":true,"isFetching":false,"error":"loader failed"}
rendered: ERROR COMPONENT: loader failed
Expected behavior
A failed background reload leaves the previously committed successful match and its rendered page in place, rather than replacing them with the error match. What the resulting invalidation and retry state should be is yours to decide.
Platform
- Router / Start Version — the same result on each of these three:
@tanstack/solid-router 2.0.0-rc.4 / @tanstack/router-core 1.171.22
@tanstack/solid-router 2.0.0-rc.5 / @tanstack/router-core 1.171.22
@tanstack/solid-router 2.0.0-rc.5 / @tanstack/router-core 1.171.27, via a pnpm override — rc.5 depends on 1.171.22, so the newest router-core has to be forced
@tanstack/solid-router 2.0.0-rc.7 / @tanstack/router-core 1.171.22
- OS: Linux
- Browser: Chromium (Playwright 1.62.1)
- Browser Version: 151.0.0.0
- Bundler: Vite
- Bundler Version: 8.2.2
Additional context
Found in a TanStack Start app that re-runs its loaders from the client on every SSE reconnect, cross-tab message and visibility resume. One lost request during such a refresh replaced a working page with the app-wide error page until a later pass succeeded. The reproducer is deliberately client-only — no Start, no SSR, no server functions, no query client — because a plain loader rejection reaches the same path; an integration reproducer is easy to add if you would like one.
Which project does this relate to? Router
Describe the bug
A failed background refresh replaces a working page with an error page.
A route loads successfully and displays its content. Later, the router re-runs its loader in the background to refresh stale data. If that loader throws, the router changes the match’s
statusto'error'and replaces the page with itserrorComponent.The previous
loaderDatais still available on the match. The router could keep showing the existing content, but instead the user loses the page they were viewing because a background refresh failed.I’d expect the existing page to stay visible, with the app deciding whether to notify the user or offer a retry. Replacing it with an error page seems appropriate when the initial load fails, but looks like a bug when a background refresh fails.
Possible cause
In
load-client,runBackground()appears to handle a loader error through the sameinstall()path used for a blocking load. It then callspublishMatches(router, projected[1]), making the error state visible and replacing the page.That publish step is skipped if another transaction has superseded the refresh, or for control outcomes such as redirects and cancellation. There doesn’t appear to be a separate check for a failed background loader that would preserve the successfully loaded page.
Complete minimal reproducer
https://github.com/TylerRick/tanstack-router-background-reload-error-repro
Steps to Reproduce the Bug
pnpm install && pnpm dev, openhttp://localhost:5596— the page rendersPAGE: FIRSTfrom a successful loader.Error, then callsrouter.invalidate()with nosync. The router hasdefaultStaleReloadMode: 'background'anddefaultStaleTime: 0.Expected behavior
A failed background reload leaves the previously committed successful match and its rendered page in place, rather than replacing them with the error match. What the resulting invalidation and retry state should be is yours to decide.
Platform
@tanstack/solid-router2.0.0-rc.4 /@tanstack/router-core1.171.22@tanstack/solid-router2.0.0-rc.5 /@tanstack/router-core1.171.22@tanstack/solid-router2.0.0-rc.5 /@tanstack/router-core1.171.27, via a pnpm override — rc.5 depends on 1.171.22, so the newest router-core has to be forced@tanstack/solid-router 2.0.0-rc.7 / @tanstack/router-core 1.171.22Additional context
Found in a TanStack Start app that re-runs its loaders from the client on every SSE reconnect, cross-tab message and visibility resume. One lost request during such a refresh replaced a working page with the app-wide error page until a later pass succeeded. The reproducer is deliberately client-only — no Start, no SSR, no server functions, no query client — because a plain loader rejection reaches the same path; an integration reproducer is easy to add if you would like one.