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 background (stale-while-revalidate) reload whose loader rejects publishes the failed lane: the committed successful match becomes status: 'error' and the route's errorComponent replaces its component. The match keeps the loaderData its page was rendering from, so nothing was lost — it was replaced. A refresh the user never asked for takes their page away.

Where it appears to happen: runBackground() in load-client reduces the lane (installing the error at the boundary through the same install() path a blocking lane uses), projects it, then calls publishMatches(router, projected[1]) unconditionally — its only bail-outs are a superseded transaction (router._tx !== tx || router._committed !== base) and control outcomes (redirect/cancel). A lane whose loader rejected is published like any other.

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.

Screenshots or Videos

n/a

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
  • 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