TanStack Router 11 ⚛️ Error Handling — Error components & boundaries
The previous chapters covered the data loaders and the path parameters, which are the places where the runtime errors originate: the network failure, the invalid param, the missing resource. This chapter covers the mechanism that catches those errors and renders the fallback UI: the error components and the boundaries. In the TanStack Router, the route object has the errorComponent property. When the loader or the component throws, the router finds the nearest errorComponent in the route hierarchy and renders it instead of the broken route. The critical distinction is between the errorComponent and the notFoundComponent. The errorComponent catches the thrown errors. The notFoundComponent catches the specific “not found” condition triggered by the notFound() function in the loader or the parse. The two are not interchangeable: the not-found condition is a valid result, not an error, and the UI is different.
Key point: The errorComponent is the route property. It receives the error prop with the thrown error. The notFoundComponent is the route property. It is triggered by the throw notFound() in the loader or the params.parse. The ErrorComponent prop is the alternative for the functional component. The defaultErrorComponent and defaultNotFoundComponent in the router options are the global fallbacks. The useRouter hook provides the invalidate method for the retry. The useMatch hook provides the error state for the specific route.
Why error handling requires a specific mental model
The error versus not-found problem. The notFound() is the explicit signal that the resource does not exist. It is not an error: the URL is valid, the route matches, but the data is not there. The notFoundComponent renders the “404” page. The errorComponent is the unexpected failure: the network is down, the server returned 500, the component has a bug. The two are the different states with the different recovery options. The notFoundComponent typically offers the “go home” link; the errorComponent typically offers the “retry” button.
The error propagation problem. The error thrown in the loader propagates to the nearest errorComponent in the route hierarchy. If the /users/$userId/posts/$postId route throws, the router checks the /users/$userId/posts/$postId route’s errorComponent first, then the /users/$userId/posts parent, then the /users/$userId, then the root. The first errorComponent found is rendered. The defaultErrorComponent in the router options is the last resort.
The error prop problem. The errorComponent receives the error prop. The error is the thrown value: the Error instance, the Response, or the unknown object. The component must distinguish the types. The Error has the message and the stack. The Response has the status and the statusText. The isRouteErrorResponse helper is the TanStack Router equivalent of the React Router’s helper.
The retry problem. The router.invalidate() re-runs the loaders for the current route. The errorComponent with the retry button calls the invalidate, and the loader runs again. The successful retry replaces the error component with the actual content. The useRouter hook provides the router instance.
The notFound in the parse problem. The params.parse function can throw the notFound() if the param is invalid. The /users/abc route matches the /users/$id route, but the parse throws notFound() because the abc is not a valid ID. The notFoundComponent renders the 404 page. The alternative is the redirect() to the error page, but the notFound() is the declarative.
The ErrorComponent versus errorComponent problem. The errorComponent is the option in the route definition: errorComponent: MyError. The ErrorComponent is the same thing (the case sensitivity depends on the version). The errorComponent is the convention. The component is the success case, the errorComponent is the failure case, the pendingComponent is the loading case, and the notFoundComponent is the missing case. The four cover the four states.
The trade-off. The route-level errorComponent is the granular. The defaultErrorComponent is the global. The route-level overrides the global. The ErrorBoundary from the React is the alternative for the non-route components. The route-level is the TanStack Router’s native mechanism.
a. The route-level errorComponent
The errorComponent is the route property. The component receives the error prop.
// src/routes/posts.$postId.tsx
import { createFileRoute, useRouter } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const res = await fetch(`/api/posts/${params.postId}`)
if (!res.ok) throw new Error(`Failed: ${res.status}`)
return res.json()
},
component: PostDetail,
errorComponent: PostError,
})
function PostDetail() {
const post = Route.useLoaderData()
return <h1>{post.title}</h1>
}
function PostError({ error }: { error: Error }) {
const router = useRouter()
return (
<div>
<h1>Failed to load the post</h1>
<pre>{error.message}</pre>
<button onClick={() => router.invalidate()}>Retry</button>
</div>
)
}
The PostError renders when the loader throws. The error.message is the thrown error message. The router.invalidate() re-runs the loader, which is the retry.
The error prop is typed. The Error type is the default. If the loader throws the Response, the type is different and the helper is required.
import { isRouteErrorResponse } from '@tanstack/react-router'
function PostError({ error }: { error: unknown }) {
if (isRouteErrorResponse(error)) {
return <h1>{error.status} {error.statusText}</h1>
}
if (error instanceof Error) {
return <h1>{error.message}</h1>
}
return <h1>Unknown error</h1>
}
The isRouteErrorResponse(error) returns true for the thrown Response. The error.status and the error.statusText are the fields. The error instanceof Error is the check for the regular errors. The fallback is the unknown.
b. The notFoundComponent and the notFound() function
The notFound() function signals that the resource does not exist. The notFoundComponent renders the 404 page.
// src/routes/posts.$postId.tsx
import { createFileRoute, notFound, Link } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const res = await fetch(`/api/posts/${params.postId}`)
if (res.status === 404) throw notFound()
if (!res.ok) throw new Error('Failed')
return res.json()
},
component: PostDetail,
errorComponent: PostError,
notFoundComponent: PostNotFound,
})
function PostNotFound() {
return (
<div>
<h1>Post not found</h1>
<Link to="/posts">Back to posts</Link>
</div>
)
}
The throw notFound() is the signal. The notFoundComponent renders. The Link to="/posts" is the recovery. The errorComponent is not triggered because the not-found is not an error.
The notFound() is also thrown from the params.parse.
params: {
parse: (params) => {
const postId = Number(params.postId)
if (Number.isNaN(postId)) throw notFound()
return { postId }
},
},
The invalid param triggers the notFoundComponent. The component never sees the invalid value.
c. The global defaults and the router-level handling
The defaultErrorComponent and the defaultNotFoundComponent are the router options. They apply to the routes that do not have their own.
import { createRouter, Link } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
defaultErrorComponent: ({ error }) => (
<div>
<h1>Something went wrong</h1>
<pre>{error.message}</pre>
<Link to="/">Home</Link>
</div>
),
defaultNotFoundComponent: () => (
<div>
<h1>404 — Not Found</h1>
<Link to="/">Home</Link>
</div>
),
})
The defaultErrorComponent is the global fallback. The defaultNotFoundComponent is the global 404. The route-level errorComponent overrides the global.
The useRouter hook provides the invalidate method.
const router = useRouter()
router.invalidate()
The router.invalidate() re-runs the loaders for the current route. The error component calls it for the retry.
Complete Example Session
# ============================================
# PART 1: ROUTE ERROR COMPONENT
# ============================================
const Route = createFileRoute('/posts/$postId')({
loader: ...,
errorComponent: PostError,
})
# ============================================
# PART 2: ERROR COMPONENT
# ============================================
function PostError({ error }) {
return <h1>{error.message}</h1>
}
# ============================================
# PART 3: RETRY
# ============================================
const router = useRouter()
<button onClick={() => router.invalidate()}>Retry</button>
# ============================================
# PART 4: RESPONSE ERROR
# ============================================
import { isRouteErrorResponse } from '@tanstack/react-router'
if (isRouteErrorResponse(error)) return <h1>{error.status}</h1>
# ============================================
# PART 5: NOT FOUND
# ============================================
if (res.status === 404) throw notFound()
# ============================================
# PART 6: NOT FOUND COMPONENT
# ============================================
function PostNotFound() {
return <div><h1>404</h1><Link to="/posts">Posts</Link></div>
}
# ============================================
# PART 7: GLOBAL ERROR
# ============================================
const router = createRouter({
routeTree,
defaultErrorComponent: GlobalError,
defaultNotFoundComponent: GlobalNotFound,
})
# ============================================
# PART 8: PARAM PARSE NOT FOUND
# ============================================
params: {
parse: (p) => {
const id = Number(p.id)
if (Number.isNaN(id)) throw notFound()
return { id }
},
}
# ============================================
# PART 9: VERIFY
# ============================================
# Loader throws Error → errorComponent
# Loader throws notFound → notFoundComponent
# No route-level component → defaultErrorComponent
The nine parts trace the error handling: route error, error component, retry, response error, notFound, notFound component, global error, parse notFound, and verification.
Quick Reference
Route Options
| Option | Purpose |
|---|---|
errorComponent | Route-level error UI |
notFoundComponent | Route-level 404 UI |
pendingComponent | Route-level loading UI |
component | Success UI |
Router Options
| Option | Purpose |
|---|---|
defaultErrorComponent | Global error fallback |
defaultNotFoundComponent | Global 404 fallback |
defaultPendingComponent | Global loading fallback |
Helpers
| Function | Purpose |
|---|---|
notFound() | Signal 404 |
isRouteErrorResponse(error) | Check Response |
router.invalidate() | Retry |
Component Props
| Prop | Type |
|---|---|
error | The thrown value |
Best Practices
✅ Do This:
throw notFound()
<!-- Use notFound for the missing resources --> # ✅
router.invalidate()
<!-- Use the invalidate for the retry --> # ✅
<Link to="/">Home</Link>
<!-- Provide the recovery link --> # ✅
❌ Don’t Do This:
# Throw an Error for the 404
<!-- Use notFound() --> # ❌
# Forget the errorComponent
<!-- The loader error crashes the route --> # ❌
# Use the notFoundComponent for the errors
<!-- The not-found is not an error --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Error crashes route | No errorComponent | Add the boundary |
| 404 shows as error | Used throw new Error | Use notFound() |
| Retry not working | No invalidate | Use router.invalidate() |
Type error on error | Not typed | Use isRouteErrorResponse |
| Global fallback missing | No defaultErrorComponent | Add the fallback |
Real-World Examples
1. Route Error
errorComponent: PostError
2. Error Component
function PostError({ error }) { return <h1>{error.message}</h1>; }
3. Retry
const router = useRouter()
router.invalidate()
4. Not Found
if (res.status === 404) throw notFound()
5. Not Found Component
notFoundComponent: () => <h1>Not found</h1>
6. Global Error
defaultErrorComponent: GlobalError
7. Global Not Found
defaultNotFoundComponent: GlobalNotFound
8. Parse Not Found
if (Number.isNaN(id)) throw notFound()
9. Response Error
if (isRouteErrorResponse(error)) return <h1>{error.status}</h1>
10. Recovery Link
<Link to="/">Home</Link>
Visual
Error States
┌──────────────────────────────────────────────┐
│ Success → component │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Loading → pendingComponent │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Missing → notFoundComponent │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Error → errorComponent │
└──────────────────────────────────────────────┘
Error Hierarchy
┌──────────────────────────────────────────────┐
│ Route errorComponent (most specific) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Parent errorComponent │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ defaultErrorComponent (global) │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Route error | errorComponent |
| Route 404 | notFoundComponent |
| Route loading | pendingComponent |
| Route success | component |
| Global error | defaultErrorComponent |
| Global 404 | defaultNotFoundComponent |
| Signal 404 | throw notFound() |
| Retry | router.invalidate() |
| Check Response | isRouteErrorResponse() |
Key takeaways:
- The
errorComponentis the route-level error UI. It catches the thrown errors from the loader and the component. Theerrorprop is the thrown value. Therouter.invalidate()is the retry. - The
notFoundComponentis the 404 UI. It is triggered by thethrow notFound(), not by the errors. ThenotFound()is the explicit signal for the missing resource. - The
notFound()is thrown from the loader and the parse. Theif (res.status === 404) throw notFound()in the loader, or theif (Number.isNaN(id)) throw notFound()in theparams.parse. The component never sees the invalid value. - The global defaults are the fallbacks. The
defaultErrorComponentand thedefaultNotFoundComponentapply to the routes that do not have their own. The route-level options override the global. - The four states are distinct. The success (
component), the loading (pendingComponent), the missing (notFoundComponent), and the error (errorComponent). The four cover the four possible states of the route.
Remember: The errorComponent is the failure. The notFoundComponent is the missing. The pendingComponent is the loading. The component is the success. The notFound is the signal. The invalidate is the retry. The exam tests the four states and the distinction between the error and the not-found.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!