Which project does this relate to?
Router
Describe the bug
When a route is being preloaded (e.g. defaultPreload: 'intent') and its cached match is evicted while the preload's async work is still in flight — most commonly because the user clicks the link and actually navigates mid-preload, or because router.invalidate() / clearExpiredCache() runs — the preload pipeline crashes with an unhandled rejection:
TypeError: Cannot read properties of undefined (reading '_nonReactive')
at Re (Match-uwMccdg0.js:1:14474)
at async Promise.all (/index 1)
at async ze (Match-uwMccdg0.js:1:15299)
at async Ka.preloadRoute (Match-uwMccdg0.js:1:36895)
(minified production stack; the frames are load-matches.ts internals called from preloadRoute)
The root cause is that several code paths in packages/router-core/src/load-matches.ts re-look up the match with inner.router.getMatch(matchId) after an await and read ._nonReactive without guarding against the match having been removed from state.matches / pendingMatches / cachedMatches in the meantime, e.g. the non-null assertions here (current main):
preBeforeLoadSetup: const existingMatch = inner.router.getMatch(matchId)! followed by existingMatch._nonReactive.beforeLoadPromise
executeBeforeLoad: const match = inner.router.getMatch(matchId)! followed by match._nonReactive.loadPromise
- similar unguarded reads in the loader-run/cleanup paths
For a preload, the match only lives in cachedMatches, so any eviction during the in-flight async work (lazy chunk load, beforeLoad, loader) makes the next getMatch(matchId) return undefined and the next ._nonReactive access throw.
Navigation itself still succeeds (the real navigation builds its own matches), so the impact is an unhandled rejection that pollutes the console and error tracking (Sentry etc.) on every hover-preload-then-click race.
Steps to reproduce
- Create a router with
defaultPreload: 'intent' and a route whose loader (or lazy component chunk) takes a moment to resolve.
- Hover a
<Link> to that route so preloadRoute starts.
- Before the preload settles, click the link (or call
router.invalidate()).
- Observe the unhandled
TypeError: Cannot read properties of undefined (reading '_nonReactive') in the console.
The regression tests added in #7003 / #7006 (clearExpiredCache() / invalidate() while a preloadRoute loader is unresolved) reproduce the same race deterministically.
Expected behavior
An in-flight preload whose match has been evicted should clean up quietly (resolve/clear its controlled promises and pending timeout) instead of throwing an unhandled TypeError.
Your Example Website or App
Reproducible in production on x.com (hover the login link on the logged-out homepage, then click it while the preload is in flight).
Platform
- OS: macOS
- Browser: Chrome
- Version: @tanstack/react-router 1.170.17 / @tanstack/router-core 1.171.14 (unguarded reads still present on
main)
Additional context
There are two open PRs that appear to address exactly this: #7003 and #7006. Filing this issue since neither PR links to a tracking issue and the crash is otherwise undocumented (closest existing report is #7753, which is a different symptom of a cleared loadPromise).
Which project does this relate to?
Router
Describe the bug
When a route is being preloaded (e.g.
defaultPreload: 'intent') and its cached match is evicted while the preload's async work is still in flight — most commonly because the user clicks the link and actually navigates mid-preload, or becauserouter.invalidate()/clearExpiredCache()runs — the preload pipeline crashes with an unhandled rejection:(minified production stack; the frames are
load-matches.tsinternals called frompreloadRoute)The root cause is that several code paths in
packages/router-core/src/load-matches.tsre-look up the match withinner.router.getMatch(matchId)after anawaitand read._nonReactivewithout guarding against the match having been removed fromstate.matches/pendingMatches/cachedMatchesin the meantime, e.g. the non-null assertions here (currentmain):preBeforeLoadSetup:const existingMatch = inner.router.getMatch(matchId)!followed byexistingMatch._nonReactive.beforeLoadPromiseexecuteBeforeLoad:const match = inner.router.getMatch(matchId)!followed bymatch._nonReactive.loadPromiseFor a preload, the match only lives in
cachedMatches, so any eviction during the in-flight async work (lazy chunk load,beforeLoad, loader) makes the nextgetMatch(matchId)returnundefinedand the next._nonReactiveaccess throw.Navigation itself still succeeds (the real navigation builds its own matches), so the impact is an unhandled rejection that pollutes the console and error tracking (Sentry etc.) on every hover-preload-then-click race.
Steps to reproduce
defaultPreload: 'intent'and a route whose loader (or lazy component chunk) takes a moment to resolve.<Link>to that route sopreloadRoutestarts.router.invalidate()).TypeError: Cannot read properties of undefined (reading '_nonReactive')in the console.The regression tests added in #7003 / #7006 (
clearExpiredCache()/invalidate()while apreloadRouteloader is unresolved) reproduce the same race deterministically.Expected behavior
An in-flight preload whose match has been evicted should clean up quietly (resolve/clear its controlled promises and pending timeout) instead of throwing an unhandled TypeError.
Your Example Website or App
Reproducible in production on x.com (hover the login link on the logged-out homepage, then click it while the preload is in flight).
Platform
main)Additional context
There are two open PRs that appear to address exactly this: #7003 and #7006. Filing this issue since neither PR links to a tracking issue and the crash is otherwise undocumented (closest existing report is #7753, which is a different symptom of a cleared
loadPromise).