Which project does this relate to?
Router
Describe the bug
useMatch's strict from-based lookup throws Invariant failed: Could not find an active match from "<routeId>" when an interrupted React concurrent render pass re-renders the exiting route tree after the router has already pruned that location's match. The throw is swallowed by React's concurrent-error path (a synchronous retry succeeds once the new location's matches are in place) and surfaces only as a recoverable console error, but it fires on roughly 27% of navigations during initial page load in our application, and no component-level workaround can avoid it (details below).
Environment
@tanstack/react-router 1.170.32 (latest published at the time of this report)
@tanstack/react-start 1.168.49
@tanstack/router-core 1.171.27
- React 19 / react-dom 19, streaming SSR (TanStack Start)
- Chromium (Playwright-driven Chrome)
- Observed in a production application with a search-param-heavy streaming-SSR page
What happens
Navigating away from the page (clicking a sidebar link to another route) while the page is still hydrating causes the interrupted concurrent render pass to re-render the exiting route tree. By that point the router has pruned the exiting location's match, so the useMatch call resolving Route.useLoaderData() — which resolves internally to a strict from-based useMatch — throws:
Invariant failed: Could not find an active match from "/_portal/requests"
React recovers by synchronously rendering the entire root and logs "There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root" through onRecoverableError. The UI always recovers, but the error appears in roughly 27% of clicks during initial page load (post-upgrade measurement; see Evidence).
Reproduction shape
- Streaming-SSR page with validated search params (
/requests, search-param-heavy) served by TanStack Start.
- Load the page and wait for the app shell to hydrate.
- Click a sidebar link to navigate away mid-hydration.
- The interrupted concurrent pass re-renders the exiting tree and the invariant throws.
A throwaway Playwright spec reproduced the flake deterministically with this exact flow (4/4 desktop runs errored at the pre-upgrade frequency below). The thrown value was surfaced with temporary diagnostic patches to the installed react-dom/@tanstack/react-router distributions (reverted afterward).
Evidence
Two forms of the same race were captured, before and after upgrading the router:
Post-upgrade (current, @tanstack/react-router@1.170.32, @tanstack/react-start@1.168.49, @tanstack/router-core@1.171.27):
Invariant failed: Could not find an active match from "/_portal/requests"
- Thrown from
useMatch's strict from-based lookup when the interrupted concurrent pass renders the exiting tree after the router pruned the exiting location's match.
- Frequency: ~27% of clicks during initial page load (down from ~80% pre-upgrade; the upgrade removed the other mechanism, see below, but this sibling invariant remains).
Pre-upgrade (@tanstack/react-router@1.170.16, ~80% of runs):
Invariant failed: Could not find match for matchId "/_portal/requests/requests{"q":"","asset":"all","status":"all","type":"all","sort":"recent","view":"table"}". Please file an issue!
- Source:
dist/esm/Match.js — Match was keyed by match ID and router.stores.matchStores.get(matchId) threw the invariant when the store was missing. Match IDs embedded the route's validated search params; on navigation the router pruned the old location's matches from matchStores, while React's concurrent pass could still render the exiting tree (the memoized Match with the old matchId) before the router's store subscription settled. The store lookup missed, the invariant threw, React retried synchronously.
- Frequency: ~80% of runs — not 100%, because it is a timing race between store pruning and the interrupted concurrent pass.
The upgrade to 1.170.32 rewrote Match to be route-keyed (router.stores.getMatchStore(routeId)), so match stores persist across transitions instead of being pruned per navigation — this eliminated the matchId-store form entirely. The residual post-upgrade invariant is a sibling in the same bug class: a strict lookup inside the router's React bindings throws during the interrupted pass, when the exiting tree's match is already gone.
Why component-level remedies fail
Streaming SSR hydrates the app shell and the route's Suspense content in separate passes; a click landing between them interrupts the route subtree's hydration and triggers the same interrupted concurrent pass during initial hydration (not only during navigation). In that pass the pruned match leaves the route component no honest output:
useMatch({ from: "/_portal/requests", shouldThrow: false }) + returning null when the match is missing diverges from the server HTML — this produced hydration-mismatch console errors at a higher rate than the original throw in our measurements.
- Allowing the hook to throw (default
shouldThrow: true) logs the recoverable error described above.
We implemented the shouldThrow: false + null-render remedy and measured both outcomes; neither meets a zero-console-error bar. No component-level remedy exists — the router's React bindings throw (or cannot render honestly) during the interrupted pass, so the fix has to be in the router: the exiting tree's match data needs to remain readable (or the interrupted pass needs to be a no-op for pruned matches) for the duration of React's concurrent pass.
React recoverable-error mechanism
react-dom's concurrent-error path wraps the thrown value as Error("There was an error during concurrent rendering…", { cause: value }) and reports it through onRecoverableError when a concurrent render pass throws and a synchronous retry succeeds. The original thrown value (the router invariant) rides in cause; the browser pageerror event serializes the cause as undefined, so standard error consumers see only the wrapper text and cannot tell what was actually thrown without instrumenting react-dom.
Related issues
Which project does this relate to?
Router
Describe the bug
useMatch's strictfrom-based lookup throwsInvariant failed: Could not find an active match from "<routeId>"when an interrupted React concurrent render pass re-renders the exiting route tree after the router has already pruned that location's match. The throw is swallowed by React's concurrent-error path (a synchronous retry succeeds once the new location's matches are in place) and surfaces only as a recoverable console error, but it fires on roughly 27% of navigations during initial page load in our application, and no component-level workaround can avoid it (details below).Environment
@tanstack/react-router1.170.32 (latest published at the time of this report)@tanstack/react-start1.168.49@tanstack/router-core1.171.27What happens
Navigating away from the page (clicking a sidebar link to another route) while the page is still hydrating causes the interrupted concurrent render pass to re-render the exiting route tree. By that point the router has pruned the exiting location's match, so the
useMatchcall resolvingRoute.useLoaderData()— which resolves internally to a strictfrom-baseduseMatch— throws:React recovers by synchronously rendering the entire root and logs "There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root" through
onRecoverableError. The UI always recovers, but the error appears in roughly 27% of clicks during initial page load (post-upgrade measurement; see Evidence).Reproduction shape
/requests, search-param-heavy) served by TanStack Start.A throwaway Playwright spec reproduced the flake deterministically with this exact flow (4/4 desktop runs errored at the pre-upgrade frequency below). The thrown value was surfaced with temporary diagnostic patches to the installed
react-dom/@tanstack/react-routerdistributions (reverted afterward).Evidence
Two forms of the same race were captured, before and after upgrading the router:
Post-upgrade (current,
@tanstack/react-router@1.170.32,@tanstack/react-start@1.168.49,@tanstack/router-core@1.171.27):useMatch's strictfrom-based lookup when the interrupted concurrent pass renders the exiting tree after the router pruned the exiting location's match.Pre-upgrade (
@tanstack/react-router@1.170.16, ~80% of runs):dist/esm/Match.js—Matchwas keyed by match ID androuter.stores.matchStores.get(matchId)threw the invariant when the store was missing. Match IDs embedded the route's validated search params; on navigation the router pruned the old location's matches frommatchStores, while React's concurrent pass could still render the exiting tree (the memoizedMatchwith the oldmatchId) before the router's store subscription settled. The store lookup missed, the invariant threw, React retried synchronously.The upgrade to 1.170.32 rewrote
Matchto be route-keyed (router.stores.getMatchStore(routeId)), so match stores persist across transitions instead of being pruned per navigation — this eliminated the matchId-store form entirely. The residual post-upgrade invariant is a sibling in the same bug class: a strict lookup inside the router's React bindings throws during the interrupted pass, when the exiting tree's match is already gone.Why component-level remedies fail
Streaming SSR hydrates the app shell and the route's Suspense content in separate passes; a click landing between them interrupts the route subtree's hydration and triggers the same interrupted concurrent pass during initial hydration (not only during navigation). In that pass the pruned match leaves the route component no honest output:
useMatch({ from: "/_portal/requests", shouldThrow: false })+ returningnullwhen the match is missing diverges from the server HTML — this produced hydration-mismatch console errors at a higher rate than the original throw in our measurements.shouldThrow: true) logs the recoverable error described above.We implemented the
shouldThrow: false+ null-render remedy and measured both outcomes; neither meets a zero-console-error bar. No component-level remedy exists — the router's React bindings throw (or cannot render honestly) during the interrupted pass, so the fix has to be in the router: the exiting tree's match data needs to remain readable (or the interrupted pass needs to be a no-op for pruned matches) for the duration of React's concurrent pass.React recoverable-error mechanism
react-dom's concurrent-error path wraps the thrown value as
Error("There was an error during concurrent rendering…", { cause: value })and reports it throughonRecoverableErrorwhen a concurrent render pass throws and a synchronous retry succeeds. The original thrown value (the router invariant) rides incause; the browserpageerrorevent serializes the cause asundefined, so standard error consumers see only the wrapper text and cannot tell what was actually thrown without instrumenting react-dom.Related issues
ssr: falseroute): a related mixing of two points in time between the router's match state and React's render, surfacing insideMatchInnerImpl/Match.loaderDataoverwritten during initial client load, causing a hydration mismatch: the same window around initial hydration where router state and hydration passes interact badly.