-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Which project does this relate to?
Router
Describe the bug
Problem
When throwing a notFound() error with a custom routeId option from a route's onError callback, the error is correctly assigned to the target route's match in the router state, but the target route's notFoundComponent never renders. Instead, the application hangs in a blank state.
Root Cause
The issue stems from a mismatch between the route loading logic and the rendering logic:
-
Loading Phase (Works Correctly): The
_handleNotFoundfunction inpackages/router-core/src/load-matches.ts:53-91correctly identifies the target route and assigns the error to its match withstatus: 'notFound'. 1 -
Rendering Phase (Fails): Each route's
Matchcomponent wraps its content in aCatchNotFoundboundary. The fallback logic inpackages/react-router/src/Match.tsx:113-125checks iferror.routeId !== matchState.routeIdand re-throws the error if they don't match. 2 -
The Hang: When a child route's
onErrorthrowsnotFound({ routeId: parentRouteId }), the child route'sCatchNotFoundboundary re-throws the error because therouteIddoesn't match. This error bubbles up through parent routes, each re-throwing it for the same reason, until it reaches the target route. However, by this point, the rendering has already failed to display thenotFoundComponent.
Code References
Loading logic that works correctly:
packages/router-core/src/load-matches.ts:53-91-_handleNotFoundassigns error to correct matchpackages/router-core/src/load-matches.ts:134-137- Called fromhandleRedirectAndNotFound
Rendering logic that causes the issue:
packages/react-router/src/Match.tsx:113-125-CatchNotFoundboundary with re-throw logicpackages/react-router/src/Match.tsx:117-122- Specific condition that re-throws
Same issue exists in Solid Router:
Potential Solutions
-
Modify the re-throw logic in
CatchNotFoundto check if the current match's status is already'notFound'and render accordingly, rather than always re-throwing whenrouteIddoesn't match. -
Add special handling for
onError-originated errors that allows them to be rendered by the target route even when thrown from a child route. -
Document the limitation and recommend throwing
notFoundwith customrouteIdonly fromloader, not fromonError.
Benefits of Fixing This
-
Consistent API: The
routeIdoption would work consistently across all error throwing contexts (loader,beforeLoad, andonError). -
Better error handling flexibility: Developers could centralize not-found error handling in parent routes even when errors originate from child route error handlers.
-
Matches documentation: The not-found errors guide suggests this pattern should work but doesn't mention the
onErrorlimitation.
Workaround
Currently, the only workaround is to throw notFound({ routeId }) directly from loader instead of from onError:
export const Route = createFileRoute('/_layout/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw notFound({ routeId: '/_layout' })
return { post }
}
})Your Example Website or App
https://stackblitz.com/edit/github-ieacg2xa?file=src%2Fmain.tsx
Steps to Reproduce the Bug or Issue
// Parent route with notFoundComponent
export const Route = createFileRoute('/_layout')({
component: () => <div><Outlet /></div>,
notFoundComponent: () => <div>Not found in layout!</div>
})
// Child route that throws notFound with parent routeId in onError
export const Route = createFileRoute('/_layout/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw new Error('Post not found')
return { post }
},
onError: (error) => {
if (error.message === 'Post not found') {
// This correctly updates router state but never renders
throw notFound({ routeId: '/_layout' })
}
}
})Expected behavior
Expected: The /_layout route's notFoundComponent should render. Or at the very least the route's defaultNotFoundComponent, just like one thrown within beforeLoad does.
Actual: The app hangs with a blank screen. The router state shows the correct match has status: 'notFound', but the component never renders due to the re-throw logic.
Screenshots or Videos
No response
Platform
- Router Version: 1.132.31
Additional context
This issue affects both React Router (packages/react-router/src/Match.tsx:113-125) and Solid Router (packages/solid-router/src/Match.tsx:112-127) implementations identically, suggesting it's an architectural design decision rather than a bug. 2 3
The documentation at docs/router/framework/react/guide/not-found-errors.md:169-201 shows examples of using routeId but only from loader context, not from onError. 4