Skip to content

Conversation

Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Aug 16, 2025

The loadMatches has an unnecessary complicated async setup, probably due to how complicated the function was before recent cleanups:

async function loadMatches() {
  try {
    await new Promise((resolve, reject) => {
      ;(async () => {
        try {
          // the logic
          resolve()
        } catch (err) {
          reject(err)
        }
      })()
    })
    // after promise
  } catch (err) {
    // error handling
  }
}

Aside from some scheduling differences due to unnecessary promises in the above example, this can be simplified down to this:

async function loadMatches() {
  try {
    // the logic
    // after promise
  } catch (err) {
    // error handling
  }
}

This is what this PR does.

Summary by CodeRabbit

  • New Features

    • No user-facing changes.
  • Refactor

    • Simplified internal async flow for route loading, removing redundant promise wrapping.
    • Maintains existing behavior and public API; no action required from users.
    • More consistent error handling and slightly reduced overhead during parallel loads.
    • Improves maintainability and prepares the codebase for future enhancements.

Copy link

coderabbitai bot commented Aug 16, 2025

Walkthrough

Refactors loadMatches in packages/router-core/src/router.ts to remove an inner Promise wrapper. Keeps sequential await of beforeLoad hooks, schedules loaders in parallel via collected promises, relies on outer try/catch for errors, and awaits a readyPromise after loader resolution. No public API changes.

Changes

Cohort / File(s) Summary
Router load flow refactor
packages/router-core/src/router.ts
Simplified async control flow in loadMatches: removed nested Promise wrapper, await beforeLoad sequentially, collect and run loader promises in parallel, use outer try/catch for error handling, trigger and await readyPromise after loaders.
Test expectation update
packages/react-router/tests/store-updates-during-navigation.test.tsx
Adjusted test expectation: changed expected update count in "not found in beforeLoad" from 8 to 7.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Router
  participant Matches
  participant ReadySignal

  App->>Router: navigate()
  Router->>Router: loadMatches()
  loop for each match
    Router->>Matches: await beforeLoad(match) (sequential)
  end
  Router->>Matches: start loaders (parallel, collect promises)
  Router->>Router: await Promise.all(loaderPromises)
  Router->>ReadySignal: trigger ready()
  Router->>ReadySignal: await readyPromise
  Router-->>App: resolve navigation
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

package: react-router

Suggested reviewers

  • schiller-manuel

Poem

A hop through loaders, neat and bright,
I queued them all, then launched in flight.
Before I chew, I sniff each trail—
then many munches, all in parallel.
A twitch, a thump—“ready!” I cry. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-router-core-load-matches-microtask

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

nx-cloud bot commented Aug 16, 2025

View your CI Pipeline Execution ↗ for commit f9e3daa

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 1m 57s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 4s View ↗

☁️ Nx Cloud last updated this comment at 2025-08-16 15:23:19 UTC

Copy link

pkg-pr-new bot commented Aug 16, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@4967

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@4967

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@4967

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@4967

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@4967

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@4967

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@4967

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@4967

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@4967

@tanstack/react-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-plugin@4967

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@4967

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@4967

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@4967

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@4967

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@4967

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@4967

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@4967

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@4967

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@4967

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@4967

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@4967

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@4967

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@4967

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@4967

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@4967

@tanstack/solid-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-plugin@4967

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@4967

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@4967

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@4967

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@4967

@tanstack/start-server-functions-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-client@4967

@tanstack/start-server-functions-fetcher

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-fetcher@4967

@tanstack/start-server-functions-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-server@4967

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@4967

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@4967

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@4967

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@4967

commit: f9e3daa

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/router-core/src/router.ts (1)

2952-2960: Consider surfacing non-special errors for easier debugging

The catch branch only rethrows Redirect/NotFound. Any other unexpected error reaching here is swallowed. While most errors are handled earlier, adding a dev-only console.error (or rethrow) would aid debugging anomalous cases without changing production behavior.

For example:

     } catch (err) {
       if (isNotFound(err) && !innerLoadContext.preload) {
         const readyPromise = this.triggerOnReady(innerLoadContext)
         if (isPromise(readyPromise)) await readyPromise
         throw err
       }
       if (isRedirect(err)) {
         throw err
       }
+      if (process.env.NODE_ENV !== 'production') {
+        // Unexpected error escaping internal handlers
+        console.error(err)
+      }
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 74be286 and cb1f497.

📒 Files selected for processing (1)
  • packages/router-core/src/router.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/router-core/src/router.ts (1)
packages/router-core/src/utils.ts (1)
  • isPromise (477-485)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (1)
packages/router-core/src/router.ts (1)

2933-2937: Clearer sequential beforeLoad execution — LGTM

The explicit loop to run beforeLoad hooks sequentially is straightforward and preserves ordering guarantees.

Comment on lines +2939 to 2951
// Execute all loaders in parallel
const max =
innerLoadContext.firstBadMatchIndex ?? innerLoadContext.matches.length
for (let i = 0; i < max; i++) {
innerLoadContext.matchPromises.push(
this.loadRouteMatch(innerLoadContext, i),
)
}
await Promise.all(innerLoadContext.matchPromises)

resolveAll()
} catch (err) {
rejectAll(err)
}
})()
})
const readyPromise = this.triggerOnReady(innerLoadContext)
if (isPromise(readyPromise)) await readyPromise
} catch (err) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Potential regression: statusCode may remain 200 when a loader errors (non-redirect/notFound)

Because runLoader catches loader errors and updates match.status = 'error' without throwing, Promise.all here will typically resolve. With this structure, load()’s catch block won’t run, so the statusCode update to 500 (based on any match with status === 'error') won’t occur. Previously, the outer wrapper likely caused a rejection to flow back, triggering that status update.

Recommend propagating a sentinel error after loaders complete when any match ended in error (and not in preload mode). This preserves the existing statusCode behavior in load().

Apply this diff inside the changed block:

       for (let i = 0; i < max; i++) {
         innerLoadContext.matchPromises.push(
           this.loadRouteMatch(innerLoadContext, i),
         )
       }
-      await Promise.all(innerLoadContext.matchPromises)
+      await Promise.all(innerLoadContext.matchPromises)
+
+      // If any match errored, propagate so the caller can set statusCode = 500
+      // (do not throw during preloads).
+      if (
+        !innerLoadContext.preload &&
+        innerLoadContext.matches.some((m) => m.status === 'error')
+      ) {
+        const readyPromise0 = this.triggerOnReady(innerLoadContext)
+        if (isPromise(readyPromise0)) await readyPromise0
+        throw new Error('__TSR_LOADER_ERROR__')
+      }
 
       const readyPromise = this.triggerOnReady(innerLoadContext)
       if (isPromise(readyPromise)) await readyPromise

Alternative (if you prefer not to throw): explicitly set statusCode after await this.loadMatches(...) in load(), based on any error matches. I can draft that patch if you want.

Run this script to confirm that statusCode=500 is only set from load()’s catch and not elsewhere:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Where is statusCode assigned?
rg -nP -C3 '\bstatusCode\s*:' packages/router-core/src/router.ts

# Where do we throw/propagate generic loader errors from loadMatches?
rg -nP -C2 'loadMatches\s*=|Promise\.all|throw new Error|status === .error.' packages/router-core/src/router.ts

Length of output: 2597


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show the load() method to inspect where loadMatches is called and statusCode logic
rg -nP -C20 'loadMatches' packages/router-core/src/router.ts

Length of output: 7788


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show definition of handleRedirectAndNotFound to see statusCode logic
rg -nP -C20 'handleRedirectAndNotFound' packages/router-core/src/router.ts

Length of output: 10838


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Locate the start of load() and its catch block around statusCode updates
rg -nP "status:\s*'pending'" -C100 packages/router-core/src/router.ts

Length of output: 21738


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Extract the load() implementation and its surrounding catch for statusCode updates
sed -n '1870,2020p' packages/router-core/src/router.ts

Length of output: 4836


Ensure loader errors propagate to set statusCode = 500

Loader errors are currently swallowed by loadMatches – they mark match.status = 'error' but don’t throw, so load()’s catch never runs and statusCode stays 200. We need to re-throw after all loaders complete when any match errored (unless in preload) so the catch block can update statusCode.

Please apply this patch in packages/router-core/src/router.ts inside loadMatches (around lines 2939–2951):

       // Execute all loaders in parallel
       const max =
         innerLoadContext.firstBadMatchIndex ?? innerLoadContext.matches.length
       for (let i = 0; i < max; i++) {
         innerLoadContext.matchPromises.push(
           this.loadRouteMatch(innerLoadContext, i),
         )
       }
-      await Promise.all(innerLoadContext.matchPromises)
+      await Promise.all(innerLoadContext.matchPromises)
+
+      // Propagate a loader error so load()’s catch sets statusCode = 500
+      if (
+        !innerLoadContext.preload &&
+        innerLoadContext.matches.some((m) => m.status === 'error')
+      ) {
+        const ready = this.triggerOnReady(innerLoadContext)
+        if (isPromise(ready)) await ready
+        throw new Error('__TSR_LOADER_ERROR__')
+      }
 
       const readyPromise = this.triggerOnReady(innerLoadContext)
       if (isPromise(readyPromise)) await readyPromise

This will ensure the existing catch in load() runs and applies a 500 status when any loader fails.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Execute all loaders in parallel
const max =
innerLoadContext.firstBadMatchIndex ?? innerLoadContext.matches.length
for (let i = 0; i < max; i++) {
innerLoadContext.matchPromises.push(
this.loadRouteMatch(innerLoadContext, i),
)
}
await Promise.all(innerLoadContext.matchPromises)
resolveAll()
} catch (err) {
rejectAll(err)
}
})()
})
const readyPromise = this.triggerOnReady(innerLoadContext)
if (isPromise(readyPromise)) await readyPromise
} catch (err) {
// Execute all loaders in parallel
const max =
innerLoadContext.firstBadMatchIndex ?? innerLoadContext.matches.length
for (let i = 0; i < max; i++) {
innerLoadContext.matchPromises.push(
this.loadRouteMatch(innerLoadContext, i),
)
}
await Promise.all(innerLoadContext.matchPromises)
// Propagate a loader error so load()’s catch sets statusCode = 500
if (
!innerLoadContext.preload &&
innerLoadContext.matches.some((m) => m.status === 'error')
) {
const ready = this.triggerOnReady(innerLoadContext)
if (isPromise(ready)) await ready
throw new Error('__TSR_LOADER_ERROR__')
}
const readyPromise = this.triggerOnReady(innerLoadContext)
if (isPromise(readyPromise)) await readyPromise
} catch (err) {
🤖 Prompt for AI Agents
In packages/router-core/src/router.ts around lines 2939–2951, loadMatches
currently runs all loader promises and sets match.status = 'error' on failures
but does not throw, so load()'s catch never runs; after awaiting
Promise.all(innerLoadContext.matchPromises) and before resolving readyPromise,
check innerLoadContext.matches (or matchPromises) for any match with status ===
'error' (and skip this check if innerLoadContext.isPreload or similar preload
flag), and if any errored, re-throw an Error (or re-throw the first captured
loader error) so the outer load() catch runs and statusCode can be set to 500.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/react-router/tests/store-updates-during-navigation.test.tsx (1)

186-186: Add a short rationale to future-proof the magic number.

A brief inline note can help avoid churn if scheduling behavior changes again.

-    expect(updates).toBe(7)
+    // After removing the extra microtask in loadMatches, this case deterministically yields 7 updates.
+    expect(updates).toBe(7)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cb1f497 and f9e3daa.

📒 Files selected for processing (1)
  • packages/react-router/tests/store-updates-during-navigation.test.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (1)
packages/react-router/tests/store-updates-during-navigation.test.tsx (1)

186-186: Adjusted expectation to 7 updates is consistent with the loadMatches microtask removal — LGTM

The reduced update count matches the simplified scheduling in router-core and keeps enforcing the “no extra updates” contract.

@Sheraff Sheraff merged commit 78ef1c9 into main Aug 16, 2025
6 checks passed
@Sheraff Sheraff deleted the refactor-router-core-load-matches-microtask branch August 16, 2025 15:27
tannerlinsley pushed a commit that referenced this pull request Aug 26, 2025
The `loadMatches` has an unnecessary complicated async setup, probably
due to how complicated the function was before recent cleanups:

```ts
async function loadMatches() {
  try {
    await new Promise((resolve, reject) => {
      ;(async () => {
        try {
          // the logic
          resolve()
        } catch (err) {
          reject(err)
        }
      })()
    })
    // after promise
  } catch (err) {
    // error handling
  }
}
```

Aside from some scheduling differences due to unnecessary promises in
the above example, this can be simplified down to this:
```ts
async function loadMatches() {
  try {
    // the logic
    // after promise
  } catch (err) {
    // error handling
  }
}
```

This is what this PR does.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
  - No user-facing changes.

- Refactor
- Simplified internal async flow for route loading, removing redundant
promise wrapping.
- Maintains existing behavior and public API; no action required from
users.
- More consistent error handling and slightly reduced overhead during
parallel loads.
- Improves maintainability and prepares the codebase for future
enhancements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant