Skip to content

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

Description

@TylerRick

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions