Skip to content

Not Found in beforeLoad and Loader does not behave as expected #2139

Closed as not planned

Description

@ViewableGravy

Describe the bug

As described in the steps below, It looks like the notFound error that can be thrown from beforeLoad or the loader is not properly causing the right route to render their notFound component.

If notFound is thrown in beforeLoad, the root route will ALWAYS be the one to render the not found component. Based on router dev tools (which I have inserted above the root route so they do not unmount in my example), it looks like this is because throwing a not found error in beforeLoad causes all parent routes to also become notFound and therefore the root handles it, meaning that no layout is rendered.

If notFound is thrown in a loader, assuming that the loader ran as a result of a navigation or preload, then the notFound behaviour works as expected. However, if the loader is running on the first render of the router, this error propagates to the root route and causes the entire page to show the not found component. Interestingly, in this case, the about route doesn't actually get marked as notFound in the router state, but for some reason the root route still handles the notFound instead of the about component.

image

Your Example Website or App

https://stackblitz.com/edit/tanstack-router-cxbsxj?file=src%2Froutes%2F__root.tsx

Steps to Reproduce the Bug or Issue

  1. Load app to index route and navigate to before-load-not-found route. The root route will handle the not found instead of about, despite about being a parent and rendering an Outlet ❌
  2. Load app to index route and navigate to loader-not-found route. The about route renders it's layout and a not found in the outlet position ✅
  3. Load the app to the loader-not-found route (navigate to that page and then refresh the page). The root route handles the not found ❌

Expected behavior

  1. Load app to index route and navigate to before-load-not-found route. The about route should render it's layout and in the outlet position, the default notFound component
  2. Load app to index route and navigate to loader-not-found route. The about route renders it's layout and a not found in the outlet position
  3. Load the app to the loader-not-found route (navigate to that page and then refresh the page). The about route should render it's layout and in the outlet poisition, the default notFound component

Screenshots or Videos

No response

Platform

  • OS: [Windows]
  • Browser: [Arc]
  • Version: [Tanstack/router: 1.48.1]

Additional context

I noticed this because during my implementation of the router, I've created a middleware function (to run in beforeLoad) to check if a route is enabled for the website our application is running as. Previously this used redirects and as far as I can tell, redirects work completely fine, but may just be luck, because with a redirect, it's not really staying on the same route, so even if the same incorrect matching was to propagate up the routes, it wouldn't really matter since it's going to be redirected anyway.

In my example, I moved the Tanstack Dev tools into the InnerWrap so that they stay mounted on a not-found, helps a little.

I've also tested the following cases which behave the same:

  1. defaultNotFoundComponent manually set (createRouter & RouterProvider)
  2. Explicit not found component on about route
  3. No component set on about route

Activity

  1. ViewableGravy commented on Sep 3, 2024

    @ViewableGravy
    ContributorAuthor

    Hey, just wondering if there is any update on whether this is something that is getting looked into or aware of.

    This bug causes the use of notFound() in beforeLoad and loader to be unreliable and effectively unusable in the context they are designed for.

    If there is any other information that I can provide, let me know.

  2. nstepien commented on Oct 11, 2024

    @nstepien
    Contributor

    If notFound is thrown in beforeLoad, the root route will ALWAYS be the one to render the not found component.

    From my experience it's even worse: the RootComponent is not rendered at all.
    This leads to context providers from the RootComponent not being rendered, which then break our NotFound component, as the IntlProvider context is not available for localization.

    https://stackblitz.com/edit/github-fcvgqy?file=src%2FContext.ts,src%2Froutes%2F__root.tsx,src%2Fmain.tsx,src%2Froutes%2Fabout.tsx

    Additional context:
    Throwing any other error in beforeLoad works fine, the default error component correctly renders at the closest Outlet.

  3. Mario-Eis commented on Oct 25, 2024

    @Mario-Eis

    This issue hits us hard at the moment. In our use case we want to render "not found" for routes, the current user hast no permission for. So, according to the documentation, we do the authentication and authorization checks in beforeLoad. But throwing a notFount() there, just breaks the application. Because in our case, when rendering the root route, authentication is not done yet (we use different authentication mechanisms for different routes and inject them with router.update to the router context further down the component tree).
    And also the design system is not initialized in the root route either (also further down). So the whole thing falls back to the ugly root level 404 page too. Instead of keeping the current layout form the layout routes.

  4. nikuscs commented on Oct 29, 2024

    @nikuscs
    Contributor

    Same, trying to use a catch all on the root and throwing a notFound on the beforeLoad will indeed print out a 404 component without anything from the __root.tsx

  5. Etsija commented on Dec 30, 2024

    @Etsija

    We are seeing exactly the same problem in our app: if notFound() is thrown inside beforeLoad, our specified notFoundComponent will never be rendered. This has worked for us before, but I have no information about the router version where it stopped working.

  6. longzheng commented on Feb 3, 2025

    @longzheng

    I can confirm this beforeLoad issue still affects v1.99.3. (Throwing notFound in loader seems to work correctly)

    I'm using a workaround using a custom error class inside beforeLoad instead of notFound().

    export class NotFoundError extends Error {
      constructor(message?: string) {
        super(message);
        this.name = 'NotFoundError';
        Object.setPrototypeOf(this, NotFoundError.prototype);
      }
    }
    export const Route = createFileRoute('/about/before-load-not-found')({
      beforeLoad() {
        throw new NotFoundError();
      },
      component: () => <div>Hello /about/not-found!</div>,
      errorComponent: ({ error }) => {
        if (error instanceof NotFoundError) {
          return <>Not Found</>;
        }
    
        throw error;
      },
    });

    Demo https://stackblitz.com/edit/tanstack-router-nutwasgj?file=src%2Froutes%2Fabout.before-load-not-found.tsx

  7. chrisBas commented on Apr 7, 2025

    @chrisBas

    I experienced the same issue.

    TLDR - how to fix:

    Where you call "createRootRoute" change your code from:

    createRootRoute({
      component: () => {
        return <Providers><Outlet /></Providers>
      },
      ...
    })
    

    to:

    createRootRoute({
      component: () => {
        return <Providers><Outlet /></Providers>
      },
      notFoundComponent: () => {
        return <Providers><Outlet /></Providers>
      }
      ...
    })
    

    Explanation:

    If you have a route tree with nested routes rootRoute > childRoute > grandchildRoute and you throw an error in the grandchildRoute, the behavior is unexpected.

    • if you throw a standard error ( like throw new Error("") ) in "loader", the defaultErrorComponent from "createRouter" will run
    • if you throw a standard error ( like throw new Error("") ) in "beforeLoad", the defaultErrorComponent from "createRouter" will run
    • if you throw a notFound() error in "loader", the defaultErrorComponent from "createRouter" will run
    • BUT, if you throw a notFound() error in "beforeLoad", the notFoundComponent from "createRootRoute" will run

    If i were to guess, there is missing logic that isn't checking the defaultErrorComponent for "notFound" errors that is currently present for standard errors.

    On a side note, it seemed strange to me that defaultErrorComponent worked in general because I wouldn't think it should be wrapped with the Providers logic from createRootRoute; but it is. I imagine the logic just overwrites the Outlet with the defaultErrorComponent and then passes this to the rootRoute.

  8. natedunn commented on Jul 4, 2025

    @natedunn

    Here is context on why notFound() + beforeLoad() and layouts won't work as expected:

    https://discord.com/channels/719702312431386674/1325783958184792086

  9. nlynzaad commented on Oct 16, 2025

    @nlynzaad
    Contributor

    As mentioned in the discord discussion this is unfortunately due to how beforeLoad methods are resolved and not something can change at this moment in time.

  10. leonardo2204 commented on Oct 17, 2025

    @leonardo2204

    Here is context on why notFound() + beforeLoad() and layouts won't work as expected:

    https://discord.com/channels/719702312431386674/1325783958184792086

    I dont use discord. Is it possible to add a summary in this thread?

    Thanks

  11. rahulxdemon commented on Jan 8, 2026

    @rahulxdemon

    Here is context on why notFound() + beforeLoad() and layouts won't work as expected:

    https://discord.com/channels/719702312431386674/1325783958184792086

    I am unable to load the discord discussion

  12. ViewableGravy commented on Jan 9, 2026

    @ViewableGravy
    ContributorAuthor

    Here is context on why notFound() + beforeLoad() and layouts won't work as expected:
    https://discord.com/channels/719702312431386674/1325783958184792086

    I am unable to load the discord discussion

    The simple answer is that you cannot guarantee data for a given route is available, because before load runs synchrously, so if you throw in beforeLoad, it short circuits the rest of the pipeline.
    Image

    The comment above with the custom error boundary is likely the best solution so far

  13. rahulxdemon commented on Jan 9, 2026

    @rahulxdemon

    @ViewableGravy by throwing an error, I am able to create a custom not found error and handle it in the error component

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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