Router throws "Could not find an active match from" when an interrupted concurrent pass renders the exiting tree after match pruning
Nobody has claimed this yet.
- 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
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-router1.170.32 (latest published at the time of this report)@tanstack/react-start1.168.49@tanstack/router-core1.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 strictfrom-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—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. - 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 })+ 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.- 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
- #7910 — stale match snapshot rendered during hydration (
ssr: falseroute): a related mixing of two points in time between the router's match state and React's render, surfacing insideMatchInnerImpl/Match. - #7029 — hydrated
loaderDataoverwritten during initial client load, causing a hydration mismatch: the same window around initial hydration where router state and hydration passes interact badly. - #1368, #2417, #2501, #5346 — report the same "Could not find an active match from" invariant message, but with different root causes (route-ID/layout/index-route mismatches rather than match pruning racing an interrupted concurrent pass).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the described Playwright flow: load the streaming-SSR page, interrupt hydration by navigating away, and observe the recoverable error. Inspect dist/esm/Match.js and the React bindings around useMatch and MatchInnerImpl/Match, focusing on the pruned exiting match during the interrupted pass. Done means the exiting match remains readable or the pass becomes a no-op without the invariant or a hydration-mismatch console error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100