TanStack / TanStack/router

A failed background reload replaces the committed successful match with an error match

Open
#8,234 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
TypeScript
Stars
15.1k
Forks
1.9k
Avg merge
1d 20h
Merged PRs (30d)
143

Description

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

  1. pnpm install && pnpm dev, open http://localhost:5596 — the page renders PAGE: FIRST from a successful loader.
  2. 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.
  3. 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.

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 load-client at runBackground(), install(), and publishMatches() to trace how a failed background loader becomes visible. Run the linked reproducer with pnpm install && pnpm dev and compare the documented match snapshots. Done means a failed background reload keeps the previously committed page and successful match visible without replacing it with the error component.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, typescript, vite
Domain
frontend, testing-qa, web-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
54/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.