404 error handling broken in loaders and server functions - HTML fragments shown instead of error messages
Which project does this relate to?
Start
Describe the bug
Issue 1: throw notFound() in Loader
Description
I am encountering two main problems when a loader throws a notFound() error:
-
Client-Side Navigation: When navigating to a route where the loader throws
notFound(), the error is not correctly caught by the configurednotFoundComponentin the parent layout. Instead of showing the expected custom "Not Found" UI defined in the layout (red border, specific text), it renders a generic "Not Found" message (simple gray box), ignoring the layout's configuration. -
Direct Navigation / SSR: When navigating directly to the URL (e.g., refreshing the page or typing the URL in the browser), the application crashes or fails to handle the request properly. Instead of rendering the application with the appropriate "Not Found" state, the server returns a raw error, such as
Cannot GET /repro-loader-issue.
Issue 2: Server Function throws not found errors
Description
I am encountering issues when a Server Function returns a 404 error and is consumed via useSuspenseQuery with throwOnError: true.
I am testing two different ways of returning a 404 from a server function:
return new Response('Personal information not found', { status: 404 })setResponseStatus(404); throw new Error('Personal information not found')
Expected Behavior:
The error message ("Personal information not found") should be caught and displayed in the ErrorBoundary component. The error should be properly propagated through React Query and caught by the error boundary.
Observed Behavior:
-
Client-Side Navigation: When navigating to these routes via client-side navigation (clicking the links), instead of showing the expected error message, the
ErrorBoundarydisplays an HTML fragment string like:Error: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot GET /_serverFn/eyJmaWxlIjoiL0BpZC9zcmMvc2VydmVyL3JlcHJvLWZucy50cz90c3ItZGlyZWN0aXZlLXVzZS1zZXJ2ZXI9IiwiZXhwb3J0Ijoic2V0U3RhdHVzVGhyb3dGbl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0</pre> </body> </html> -
Direct Navigation / SSR (Return Response 404): When navigating directly to the URL or refreshing the page on the
return-response-404route, the server crashes completely and stops responding. -
Direct Navigation / SSR (Set Status & Throw): When refreshing the page multiple times on the
set-status-throwroute, the behavior is inconsistent:- Most of the time (~9 out of 10 refreshes): Shows the HTML fragment error mentioned above
- Occasionally (~1 out of 10 refreshes): Shows the correct error message "Personal information not found"
Your Example Website or App
https://github.com/mailok/tanstack-testing
Steps to Reproduce the Bug or Issue
Issue 1
- Open the application at the root
/. - Click the "Test Loader notFound()" link.
- Observation: Check if the
notFoundComponentfrom the layout is rendered.
- Observation: Check if the
- Navigate directly to
http://localhost:3000/repro-loader-issue(or the corresponding port).- Observation: The server returns
Cannot GET /repro-loader-issue(or similar 404 error) instead of the React application.
- Observation: The server returns
Relevant Files:
Issue 2
- Open the application at the root
/. - Click on "Return Response 404" link.
- Observation: Error message shows HTML fragment instead of "Personal information not found"
- Refresh the page.
- Observation: Server crashes
- Restart the server and navigate to
/. - Click on "Set Status & Throw" link.
- Observation: Error message shows HTML fragment instead of "Personal information not found"
- Refresh the page multiple times.
- Observation: Inconsistent behavior - mostly HTML fragment, occasionally the correct error message
Relevant Files:
- Server Functions
- Layout with notFoundComponent
- Return Response 404 route
- Set Status & Throw route
- Home page with test links
Expected behavior
_
Screenshots or Videos
No response
Platform
- @tanstack/react-start": "^1.139.3",
- @tanstack/react-router": "^1.139.3"
- @tanstack/react-query": "^5.90.10"
- @tanstack/react-router-ssr-query": "^1.139.3"
- "nitro": "latest",
- OS: [e.g. macOS, Windows, Linux]
- Browser: Chrome
- Browser Version: 142.0.7444.60
- Bundler: vite
- Bundler Version: ^7.1.7
Additional context
No response
We are reporting the same issue Route with server function as loader that throws notFound crashes route on HMR
We are reporting the same issue Route with server function as loader that throws notFound crashes route on HMR
Hey @Andresuito,
Thanks for pointing out that our issues might overlap. I initially thought we were hitting the same underlying bug, but after digging into the scenarios I reported I’m increasingly of the view that they may actually be different or broader. Here’s a breakdown of where I see divergence:
Your issue:
• A route with a loader (server-fn) that throws notFound() crashes only when using HMR.
• The failure mode you highlight is specifically tied to HMR triggering after a module save, and the error shows {"isNotFound": true} rather than the expected NotFoundComponent. 
• So your focus is essentially: “loader throws notFound → HMR triggers → route crashes”.
My issue:
• I’m seeing multiple failure modes, not just the HMR case:
• When a loader throws notFound() during client-side navigation, the configured notFoundComponent in a layout is not rendered, instead a generic “Not Found” UI appears. 
• On direct navigation/SSR (refresh or entering URL) when loader throws notFound(), the server returns Cannot GET /… instead of rendering the app’s “Not Found” state. 
• Also: When a server function returns or throws a 404 (via Response or throwing Error) and is consumed via useSuspenseQuery, I observe HTML fragments instead of proper error boundaries rendering the error message. 
• The contexts are broader: client navigation, SSR/direct access, server functions, not just HMR.
So while there is overlap (loader throws notFound), the symptoms and contexts differ: HMR vs non-HMR, loader vs server-fn, crash vs mis-render vs generic fallback.
It may still be the same root cause (e.g., error boundary / notFound flow mis-wired), but given the variation of failure modes I think we should treat them as distinct issue scopes or at least clearly note the additional paths I’m seeing.
What do you think?
Maybe it's worth adding if you use setResponseStatus(404) instead of returning notFound, this error comes up. Maybe the server it's crashing so it can't wire the notFoundComponent with the response.
2:47:44 AM [vite] Internal server error: Failed to resolve import "tanstack-start-injected-head-scripts:v" from "node_modules/@tanstack/start-server-core/dist/esm/router-manifest.js?v=bbb81834". Does the file exist?
Plugin: vite:import-analysis
File: /path/to/project/node_modules/@tanstack/start-server-core/dist/esm/router-manifest.js?v=bbb81834:22:6
7 | let script = `import('${startManifest.clientEntry}')`;
8 | if (process.env.TSS_DEV_SERVER === "true") {
9 | const { injectedHeadScripts } = await import("tanstack-start-injected-head-scripts:v");
| ^
10 | if (injectedHeadScripts) {
11 | script = `${injectedHeadScripts + ";"}${script}`;
at _formatLog (/path/to/project/node_modules/vite/dist/node/chunks/config.js:28998:47)
at error (/path/to/project/node_modules/vite/dist/node/chunks/config.js:28995:14)
at <anonymous> (/path/to/project/node_modules/vite/dist/node/chunks/config.js:27176:38)
at transform (/path/to/project/node_modules/vite/dist/node/chunks/config.js:27144:18)
at processTicksAndRejections (native) (x3)