Skip to content

notFound() with custom routeId in onError callback doesn't render target route's notFoundComponent #5546

@Rendez

Description

@Rendez

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:

  1. Loading Phase (Works Correctly): The _handleNotFound function in packages/router-core/src/load-matches.ts:53-91 correctly identifies the target route and assigns the error to its match with status: 'notFound'. 1

  2. Rendering Phase (Fails): Each route's Match component wraps its content in a CatchNotFound boundary. The fallback logic in packages/react-router/src/Match.tsx:113-125 checks if error.routeId !== matchState.routeId and re-throws the error if they don't match. 2

  3. The Hang: When a child route's onError throws notFound({ routeId: parentRouteId }), the child route's CatchNotFound boundary re-throws the error because the routeId doesn'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 the notFoundComponent.

Code References

Loading logic that works correctly:

Rendering logic that causes the issue:

Same issue exists in Solid Router:

Potential Solutions

  1. Modify the re-throw logic in CatchNotFound to check if the current match's status is already 'notFound' and render accordingly, rather than always re-throwing when routeId doesn't match.

  2. Add special handling for onError-originated errors that allows them to be rendered by the target route even when thrown from a child route.

  3. Document the limitation and recommend throwing notFound with custom routeId only from loader, not from onError.

Benefits of Fixing This

  1. Consistent API: The routeId option would work consistently across all error throwing contexts (loader, beforeLoad, and onError).

  2. Better error handling flexibility: Developers could centralize not-found error handling in parent routes even when errors originate from child route error handlers.

  3. Matches documentation: The not-found errors guide suggests this pattern should work but doesn't mention the onError limitation.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions