npx skills add ...
npx skills add tanstack/router --skill not-found-and-errors
notFound() function, notFoundComponent, defaultNotFoundComponent, notFoundMode (fuzzy/root), errorComponent, CatchBoundary, CatchNotFound, isNotFound, NotFoundRoute (deprecated), route masking (mask option, createRouteMask, unmaskOnReload).
npx skills add tanstack/router --skill not-found-and-errors
TanStack Router handles two categories of "not found": unmatched URL paths (automatic) and missing resources like a post that doesn't exist (manual via notFound()). Error boundaries are configured per-route via errorComponent.
CRITICAL: Do NOT use the deprecated
NotFoundRoute. When present,notFound()andnotFoundComponentwill NOT work. Remove it and usenotFoundComponentinstead. CRITICAL:useLoaderDatamay be undefined insidenotFoundComponent. UseuseParams,useSearch, oruseRouteContextinstead.
notFoundComponent on Root RoutedefaultNotFoundComponentnotFound()Throw notFound() in loader or beforeLoad when a resource doesn't exist. It works like redirect() — throw it to trigger the not-found boundary.
notFound({ routeId })You can force a specific parent route to handle the not-found error:
rootRouteIdnotFoundMode: Fuzzy vs Rootfuzzy (default)The router finds the nearest parent route with children and a notFoundComponent. Preserves as much parent layout as possible.
Given routes: __root__ → posts → $postId, accessing /posts/1/edit:
<Root> renders<Posts> renders<Posts.notFoundComponent> renders (nearest parent with children + notFoundComponent)rootAll not-found errors go to the root route's notFoundComponent, regardless of matching:
errorComponent Per RouteerrorComponent receives error, info, and reset props. For loader errors, use router.invalidate() to re-run the loader — it automatically resets the error boundary.
notFoundComponentnotFoundComponent cannot reliably use useLoaderData because the loader may not have completed. Safe hooks:
To forward partial data, use the data option on notFound():
Route masking shows a different URL in the browser bar than the actual route being rendered. Masking data is stored in location.state and is lost when the URL is shared or opened in a new tab.
<Link>useNavigatecreateRouteMaskBy default, masks survive local page reloads. To unmask on reload:
NotFoundRouteuseLoaderData in notFoundComponentOnly routes with children (and therefore an <Outlet>) can render notFoundComponent. Leaf routes (routes without children) will never catch not-found errors — the error bubbles up to the nearest parent with children.
Masking data lives in location.state (browser history). When a masked URL is copied, shared, or opened in a new tab, the masking data is lost. The browser navigates to the visible (masked) URL directly.
reset() alone instead of router.invalidate()notFound() thrown in loaders interacts with error boundaries and loader data availability. errorComponent retry requires router.invalidate().notFoundComponent data is typed as unknown; validate before use.// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => {
return (
<div>
<p>Not found!</p>
<Link to="/">Go home</Link>
</div>
)
},
})// src/routes/posts.$postId.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
import { getPost } from '../api'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post) throw notFound()
return { post }
},
component: PostComponent,
notFoundComponent: ({ data }) => {
const { postId } = Route.useParams()
return <p>Post "{postId}" not found</p>
},
})
function PostComponent() {
const { post } = Route.useLoaderData()
return <h1>{post.title}</h1>
}// src/routes/_layout/posts.$postId.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
export const Route = createFileRoute('/_layout/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post) throw notFound({ routeId: '/_layout' })
return { post }
},
})import { createFileRoute, notFound, 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 }
},
})const router = createRouter({
routeTree,
notFoundMode: 'root',
})// src/routes/posts.$postId.tsx
import { createFileRoute, useRouter } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const res = await fetch(`/api/posts/${postId}`)
if (!res.ok) throw new Error('Failed to load post')
return res.json()
},
component: PostComponent,
errorComponent: PostErrorComponent,
})
function PostErrorComponent({
error,
}: {
error: unknown
info?: { componentStack: string }
reset: () => void
}) {
const router = useRouter()
return (
<div>
<p>Error: {error instanceof Error ? error.message : String(error)}</p>
<button
onClick={() => {
// Invalidate re-runs the loader and resets the error boundary
router.invalidate()
}}
>
Retry
</button>
</div>
)
}
function PostComponent() {
const data = Route.useLoaderData()
return <h1>{data.title}</h1>
}const router = createRouter({
routeTree,
defaultErrorComponent: ({ error }) => {
const router = useRouter()
return (
<div>
<p>
Something went wrong:{' '}
{error instanceof Error ? error.message : String(error)}
</p>
<button
onClick={() => {
router.invalidate()
}}
>
Retry
</button>
</div>
)
},
})notFoundComponent: ({ data }) => {
// SAFE — always available:
const params = Route.useParams()
const search = Route.useSearch()
const context = Route.useRouteContext()
// UNSAFE — may be undefined:
// const loaderData = Route.useLoaderData()
return <p>Item {params.id} not found</p>
}loader: async ({ params }) => {
const partialData = await getPartialData(params.id)
if (!partialData.fullResource) {
throw notFound({ data: { name: partialData.name } })
}
return partialData
},
notFoundComponent: ({ data }) => {
// data is typed as unknown — validate it
const info = data as { name: string } | undefined
return <p>{info?.name ?? 'Resource'} not found</p>
},import { Link } from '@tanstack/react-router'
function PhotoGrid({ photoId }: { photoId: string }) {
return (
<Link
to="/photos/$photoId/modal"
params={{ photoId }}
mask={{
to: '/photos/$photoId',
params: { photoId },
}}
>
Open Photo
</Link>
)
}import { useNavigate } from '@tanstack/react-router'
function OpenPhotoButton({ photoId }: { photoId: string }) {
const navigate = useNavigate()
return (
<button
onClick={() =>
navigate({
to: '/photos/$photoId/modal',
params: { photoId },
mask: {
to: '/photos/$photoId',
params: { photoId },
},
})
}
>
Open Photo
</button>
)
}import { createRouter, createRouteMask } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const photoModalMask = createRouteMask({
routeTree,
from: '/photos/$photoId/modal',
to: '/photos/$photoId',
params: (prev) => ({ photoId: prev.photoId }),
})
const router = createRouter({
routeTree,
routeMasks: [photoModalMask],
})// Per-mask
const mask = createRouteMask({
routeTree,
from: '/photos/$photoId/modal',
to: '/photos/$photoId',
params: (prev) => ({ photoId: prev.photoId }),
unmaskOnReload: true,
})
// Per-link
<Link
to="/photos/$photoId/modal"
params={{ photoId }}
mask={{ to: '/photos/$photoId', params: { photoId } }}
unmaskOnReload
>
Open Photo
</Link>
// Router-wide default
const router = createRouter({
routeTree,
unmaskOnReload: true,
})// WRONG — NotFoundRoute blocks notFound() and notFoundComponent from working
import { NotFoundRoute } from '@tanstack/react-router'
const notFoundRoute = new NotFoundRoute({ component: () => <p>404</p> })
const router = createRouter({ routeTree, notFoundRoute })
// CORRECT — use notFoundComponent on root route
export const Route = createRootRoute({
component: () => <Outlet />,
notFoundComponent: () => <p>404</p>,
})// WRONG — loader may not have completed
notFoundComponent: () => {
const data = Route.useLoaderData() // may be undefined!
return <p>{data.title} not found</p>
}
// CORRECT — use safe hooks
notFoundComponent: () => {
const { postId } = Route.useParams()
return <p>Post {postId} not found</p>
}// This route has NO children — notFoundComponent here will not catch
// unmatched child paths (there are no child paths to unmatch)
export const Route = createFileRoute('/posts/$postId')({
// notFoundComponent here only works for notFound() thrown in THIS route's loader
// It does NOT catch path-based not-founds
notFoundComponent: () => <p>Not found</p>,
})// WRONG — reset() clears the error boundary but does NOT re-run the loader
function ErrorFallback({
error,
reset,
}: {
error: unknown
reset: () => void
}) {
return <button onClick={reset}>Retry</button>
}
// CORRECT — invalidate re-runs loaders and resets the error boundary
function ErrorFallback({ error }: { error: unknown; reset: () => void }) {
const router = useRouter()
return (
<button
onClick={() => {
router.invalidate()
}}
>
Retry
</button>
)
}