Skip to content

TypeError: Cannot read properties of undefined (reading '_nonReactive') from preloadRoute when an in-flight preload's match is evicted #7759

Description

@anonrig

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

  1. Create a router with defaultPreload: 'intent' and a route whose loader (or lazy component chunk) takes a moment to resolve.
  2. Hover a <Link> to that route so preloadRoute starts.
  3. Before the preload settles, click the link (or call router.invalidate()).
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions