⚠️ This page covers the newer notFound function and notFoundComponent API for handling not found errors. The NotFoundRoute route is deprecated and will be removed in a future release. See Migrating from NotFoundRoute for more information.
There are 2 uses for not-found errors in TanStack Router:
Under the hood, both of these cases are implemented using the same notFound function and notFoundComponent API.
When TanStack Router encounters a pathname that doesn't match any known route pattern OR partially matches a route pattern but with extra trailing pathname segments, it will automatically throw a not-found error.
Depending on the notFoundMode option, the router will handle these automatic errors differently::
By default, the router's notFoundMode is set to fuzzy, which indicates that if a pathname doesn't match any known route, the router will attempt to use the closest matching route with children/(an outlet) and a configured not found component.
❓ Why is this the default? Fuzzy matching to preserve as much parent layout as possible for the user gives them more context to navigate to a useful location based on where they thought they would arrive.
The nearest suitable route is found using the following criteria:
For example, consider the following route tree:
If provided the path of /posts/1/edit, the following component structure will be rendered:
The notFoundComponent of the posts route will be rendered because it is the nearest suitable parent route with children (and therefore an outlet) and a notFoundComponent configured.
When notFoundMode is set to root, all not-found errors will be handled by the root route's notFoundComponent instead of bubbling up from the nearest fuzzy-matched route.
For example, consider the following route tree:
If provided the path of /posts/1/edit, the following component structure will be rendered:
The notFoundComponent of the __root__ route will be rendered because the notFoundMode is set to root.
To handle both types of not-found errors, you can attach a notFoundComponent to a route. This component will be rendered when a not-found error is thrown.
For example, configuring a notFoundComponent for a /settings route to handle non-existing settings pages:
export const Route = createFileRoute('/settings')({
component: () => {
return (
<div>
<p>Settings page</p>
<Outlet />
</div>
)
},
notFoundComponent: () => {
return <p>This setting page doesn't exist!</p>
},
})
export const Route = createFileRoute('/settings')({
component: () => {
return (
<div>
<p>Settings page</p>
<Outlet />
</div>
)
},
notFoundComponent: () => {
return <p>This setting page doesn't exist!</p>
},
})
Or configuring a notFoundComponent for a /posts/$postId route to handle posts that don't exist:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post) throw notFound()
return { post }
},
component: ({ post }) => {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
)
},
notFoundComponent: () => {
return <p>Post not found!</p>
},
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post) throw notFound()
return { post }
},
component: ({ post }) => {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
)
},
notFoundComponent: () => {
return <p>Post not found!</p>
},
})
You may want to provide a default not-found component for every route in your app with child routes.
Why only routes with children? Leaf-node routes (routes without children) will never render an Outlet and therefore are not able to handle not-found errors.
To do this, pass a defaultNotFoundComponent to the createRouter function:
const router = createRouter({
defaultNotFoundComponent: () => {
return (
<div>
<p>Not found!</p>
<Link to="/">Go home</Link>
</div>
)
},
})
const router = createRouter({
defaultNotFoundComponent: () => {
return (
<div>
<p>Not found!</p>
<Link to="/">Go home</Link>
</div>
)
},
})
You can manually throw not-found errors in loader methods and components using the notFound function. This is useful when you need to signal that a resource cannot be found.
The notFound function works in a similar fashion to the redirect function. To cause a not-found error, you can throw a notFound().
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
// Returns `null` if the post doesn't exist
const post = await getPost(postId)
if (!post) {
throw notFound()
// Alternatively, you can make the notFound function throw:
// notFound({ throw: true })
}
// Post is guaranteed to be defined here because we threw an error
return { post }
},
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
// Returns `null` if the post doesn't exist
const post = await getPost(postId)
if (!post) {
throw notFound()
// Alternatively, you can make the notFound function throw:
// notFound({ throw: true })
}
// Post is guaranteed to be defined here because we threw an error
return { post }
},
})
The not-found error above will be handled by the same route or nearest parent route that has either a notFoundComponent route option or the defaultNotFoundComponent router option configured.
If neither the route nor any suitable parent route is found to handle the error, the root route will handle it using TanStack Router's extremely basic (and purposefully undesirable) default not-found component that simply renders <div>Not Found</div>. It's highly recommended to either attach at least one notFoundComponent to the root route or configure a router-wide defaultNotFoundComponent to handle not-found errors.
Sometimes you may want to trigger a not-found on a specific parent route and bypass the normal not-found component propagation. To do this, pass in a route id to the route option in the notFound function.
// _layout.tsx
export const Route = createFileRoute('/_layout')({
// This will render
notFoundComponent: () => {
return <p>Not found (in _layout)</p>
},
component: () => {
return (
<div>
<p>This is a layout!</p>
<Outlet />
</div>
)
},
})
// _layout/a.tsx
export const Route = createFileRoute('/_layout/a')({
loader: async () => {
// This will make LayoutRoute handle the not-found error
throw notFound({ routeId: '/_layout' })
// ^^^^^^^^^ This will autocomplete from the registered router
},
// This WILL NOT render
notFoundComponent: () => {
return <p>Not found (in _layout/a)</p>
},
})
// _layout.tsx
export const Route = createFileRoute('/_layout')({
// This will render
notFoundComponent: () => {
return <p>Not found (in _layout)</p>
},
component: () => {
return (
<div>
<p>This is a layout!</p>
<Outlet />
</div>
)
},
})
// _layout/a.tsx
export const Route = createFileRoute('/_layout/a')({
loader: async () => {
// This will make LayoutRoute handle the not-found error
throw notFound({ routeId: '/_layout' })
// ^^^^^^^^^ This will autocomplete from the registered router
},
// This WILL NOT render
notFoundComponent: () => {
return <p>Not found (in _layout/a)</p>
},
})
You can also target the root route by passing the exported rootRouteId variable to the notFound function's route property:
import { rootRouteId } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post) throw notFound({ routeId: rootRouteId })
return { post }
},
})
import { rootRouteId } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post) throw notFound({ routeId: rootRouteId })
return { post }
},
})
You can also throw not-found errors in components. However, it is recommended to throw not-found errors in loader methods instead of components in order to correctly type loader data and prevent flickering.
TanStack Router exposes a CatchNotFound component similar to CatchBoundary that can be used to catch not-found errors in components and display UI accordingly.
notFoundComponent is a special case when it comes to data loading. SomeRoute.useLoaderData may not be defined depending on which route you are trying to access and where the not-found error gets thrown. However, Route.useParams, Route.useSearch, Route.useRouteContext, etc. will return a defined value.
If you need to pass incomplete loader data to notFoundComponent, pass the data via the data option in the notFound function and validate it in notFoundComponent.
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post)
throw notFound({
// Forward some data to the notFoundComponent
// data: someIncompleteLoaderData
})
return { post }
},
// `data: unknown` is passed to the component via the `data` option when calling `notFound`
notFoundComponent: ({ data }) => {
// ❌ useLoaderData is not valid here: const { post } = Route.useLoaderData()
// ✅:
const { postId } = Route.useParams()
const search = Route.useSearch()
const context = Route.useRouteContext()
return <p>Post with id {postId} not found!</p>
},
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post)
throw notFound({
// Forward some data to the notFoundComponent
// data: someIncompleteLoaderData
})
return { post }
},
// `data: unknown` is passed to the component via the `data` option when calling `notFound`
notFoundComponent: ({ data }) => {
// ❌ useLoaderData is not valid here: const { post } = Route.useLoaderData()
// ✅:
const { postId } = Route.useParams()
const search = Route.useSearch()
const context = Route.useRouteContext()
return <p>Post with id {postId} not found!</p>
},
})
See SSR guide for more information.
The NotFoundRoute API is deprecated in favor of notFoundComponent. The NotFoundRoute API will be removed in a future release.
The notFound function and notFoundComponent will not work when using NotFoundRoute.
The main differences are:
To migrate from NotFoundRoute to notFoundComponent, follow these steps:
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.