@tanstack/router-core 1.167.3 → 1.167.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,435 @@
1
+ ---
2
+ name: router-core/not-found-and-errors
3
+ description: >-
4
+ notFound() function, notFoundComponent, defaultNotFoundComponent,
5
+ notFoundMode (fuzzy/root), errorComponent, CatchBoundary,
6
+ CatchNotFound, isNotFound, NotFoundRoute (deprecated), route
7
+ masking (mask option, createRouteMask, unmaskOnReload).
8
+ type: sub-skill
9
+ library: tanstack-router
10
+ library_version: '1.166.2'
11
+ requires:
12
+ - router-core
13
+ sources:
14
+ - TanStack/router:docs/router/guide/not-found-errors.md
15
+ - TanStack/router:docs/router/guide/route-masking.md
16
+ ---
17
+
18
+ # Not Found and Errors
19
+
20
+ 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`.
21
+
22
+ > **CRITICAL**: Do NOT use the deprecated `NotFoundRoute`. When present, `notFound()` and `notFoundComponent` will NOT work. Remove it and use `notFoundComponent` instead.
23
+ > **CRITICAL**: `useLoaderData` may be undefined inside `notFoundComponent`. Use `useParams`, `useSearch`, or `useRouteContext` instead.
24
+
25
+ ## Not Found Handling
26
+
27
+ ### Global 404: `notFoundComponent` on Root Route
28
+
29
+ ```tsx
30
+ // src/routes/__root.tsx
31
+ import { createRootRoute, Outlet, Link } from '@tanstack/react-router'
32
+
33
+ export const Route = createRootRoute({
34
+ component: () => <Outlet />,
35
+ notFoundComponent: () => {
36
+ return (
37
+ <div>
38
+ <h1>404 — Page Not Found</h1>
39
+ <Link to="/">Go Home</Link>
40
+ </div>
41
+ )
42
+ },
43
+ })
44
+ ```
45
+
46
+ ### Router-Wide Default: `defaultNotFoundComponent`
47
+
48
+ ```tsx
49
+ // src/router.tsx
50
+ import { createRouter } from '@tanstack/react-router'
51
+ import { routeTree } from './routeTree.gen'
52
+
53
+ const router = createRouter({
54
+ routeTree,
55
+ defaultNotFoundComponent: () => {
56
+ return (
57
+ <div>
58
+ <p>Not found!</p>
59
+ <Link to="/">Go home</Link>
60
+ </div>
61
+ )
62
+ },
63
+ })
64
+ ```
65
+
66
+ ### Per-Route 404: Missing Resources with `notFound()`
67
+
68
+ Throw `notFound()` in `loader` or `beforeLoad` when a resource doesn't exist. It works like `redirect()` — throw it to trigger the not-found boundary.
69
+
70
+ ```tsx
71
+ // src/routes/posts.$postId.tsx
72
+ import { createFileRoute, notFound } from '@tanstack/react-router'
73
+ import { getPost } from '../api'
74
+
75
+ export const Route = createFileRoute('/posts/$postId')({
76
+ loader: async ({ params: { postId } }) => {
77
+ const post = await getPost(postId)
78
+ if (!post) throw notFound()
79
+ return { post }
80
+ },
81
+ component: PostComponent,
82
+ notFoundComponent: ({ data }) => {
83
+ const { postId } = Route.useParams()
84
+ return <p>Post "{postId}" not found</p>
85
+ },
86
+ })
87
+
88
+ function PostComponent() {
89
+ const { post } = Route.useLoaderData()
90
+ return <h1>{post.title}</h1>
91
+ }
92
+ ```
93
+
94
+ ### Targeting a Specific Route with `notFound({ routeId })`
95
+
96
+ You can force a specific parent route to handle the not-found error:
97
+
98
+ ```tsx
99
+ // src/routes/_layout/posts.$postId.tsx
100
+ import { createFileRoute, notFound } from '@tanstack/react-router'
101
+
102
+ export const Route = createFileRoute('/_layout/posts/$postId')({
103
+ loader: async ({ params: { postId } }) => {
104
+ const post = await getPost(postId)
105
+ if (!post) throw notFound({ routeId: '/_layout' })
106
+ return { post }
107
+ },
108
+ })
109
+ ```
110
+
111
+ ### Targeting Root Route with `rootRouteId`
112
+
113
+ ```tsx
114
+ import { createFileRoute, notFound, rootRouteId } from '@tanstack/react-router'
115
+
116
+ export const Route = createFileRoute('/posts/$postId')({
117
+ loader: async ({ params: { postId } }) => {
118
+ const post = await getPost(postId)
119
+ if (!post) throw notFound({ routeId: rootRouteId })
120
+ return { post }
121
+ },
122
+ })
123
+ ```
124
+
125
+ ## `notFoundMode`: Fuzzy vs Root
126
+
127
+ ### `fuzzy` (default)
128
+
129
+ The router finds the nearest parent route with children and a `notFoundComponent`. Preserves as much parent layout as possible.
130
+
131
+ Given routes: `__root__` → `posts` → `$postId`, accessing `/posts/1/edit`:
132
+
133
+ - `<Root>` renders
134
+ - `<Posts>` renders
135
+ - `<Posts.notFoundComponent>` renders (nearest parent with children + notFoundComponent)
136
+
137
+ ### `root`
138
+
139
+ All not-found errors go to the root route's `notFoundComponent`, regardless of matching:
140
+
141
+ ```tsx
142
+ const router = createRouter({
143
+ routeTree,
144
+ notFoundMode: 'root',
145
+ })
146
+ ```
147
+
148
+ ## Error Handling
149
+
150
+ ### `errorComponent` Per Route
151
+
152
+ `errorComponent` receives `error`, `info`, and `reset` props. For loader errors, use `router.invalidate()` to re-run the loader — it automatically resets the error boundary.
153
+
154
+ ```tsx
155
+ // src/routes/posts.$postId.tsx
156
+ import { createFileRoute, useRouter } from '@tanstack/react-router'
157
+
158
+ export const Route = createFileRoute('/posts/$postId')({
159
+ loader: async ({ params: { postId } }) => {
160
+ const res = await fetch(`/api/posts/${postId}`)
161
+ if (!res.ok) throw new Error('Failed to load post')
162
+ return res.json()
163
+ },
164
+ component: PostComponent,
165
+ errorComponent: PostErrorComponent,
166
+ })
167
+
168
+ function PostErrorComponent({
169
+ error,
170
+ }: {
171
+ error: Error
172
+ info: { componentStack: string }
173
+ reset: () => void
174
+ }) {
175
+ const router = useRouter()
176
+
177
+ return (
178
+ <div>
179
+ <p>Error: {error.message}</p>
180
+ <button
181
+ onClick={() => {
182
+ // Invalidate re-runs the loader and resets the error boundary
183
+ router.invalidate()
184
+ }}
185
+ >
186
+ Retry
187
+ </button>
188
+ </div>
189
+ )
190
+ }
191
+
192
+ function PostComponent() {
193
+ const data = Route.useLoaderData()
194
+ return <h1>{data.title}</h1>
195
+ }
196
+ ```
197
+
198
+ ### Router-Wide Default Error Component
199
+
200
+ ```tsx
201
+ const router = createRouter({
202
+ routeTree,
203
+ defaultErrorComponent: ({ error }) => {
204
+ const router = useRouter()
205
+ return (
206
+ <div>
207
+ <p>Something went wrong: {error.message}</p>
208
+ <button
209
+ onClick={() => {
210
+ router.invalidate()
211
+ }}
212
+ >
213
+ Retry
214
+ </button>
215
+ </div>
216
+ )
217
+ },
218
+ })
219
+ ```
220
+
221
+ ## Data in `notFoundComponent`
222
+
223
+ `notFoundComponent` cannot reliably use `useLoaderData` because the loader may not have completed. Safe hooks:
224
+
225
+ ```tsx
226
+ notFoundComponent: ({ data }) => {
227
+ // SAFE — always available:
228
+ const params = Route.useParams()
229
+ const search = Route.useSearch()
230
+ const context = Route.useRouteContext()
231
+
232
+ // UNSAFE — may be undefined:
233
+ // const loaderData = Route.useLoaderData()
234
+
235
+ return <p>Item {params.id} not found</p>
236
+ }
237
+ ```
238
+
239
+ To forward partial data, use the `data` option on `notFound()`:
240
+
241
+ ```tsx
242
+ loader: async ({ params }) => {
243
+ const partialData = await getPartialData(params.id)
244
+ if (!partialData.fullResource) {
245
+ throw notFound({ data: { name: partialData.name } })
246
+ }
247
+ return partialData
248
+ },
249
+ notFoundComponent: ({ data }) => {
250
+ // data is typed as unknown — validate it
251
+ const info = data as { name: string } | undefined
252
+ return <p>{info?.name ?? 'Resource'} not found</p>
253
+ },
254
+ ```
255
+
256
+ ## Route Masking
257
+
258
+ 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.
259
+
260
+ ### Imperative Masking on `<Link>`
261
+
262
+ ```tsx
263
+ import { Link } from '@tanstack/react-router'
264
+
265
+ function PhotoGrid({ photoId }: { photoId: string }) {
266
+ return (
267
+ <Link
268
+ to="/photos/$photoId/modal"
269
+ params={{ photoId }}
270
+ mask={{
271
+ to: '/photos/$photoId',
272
+ params: { photoId },
273
+ }}
274
+ >
275
+ Open Photo
276
+ </Link>
277
+ )
278
+ }
279
+ ```
280
+
281
+ ### Imperative Masking with `useNavigate`
282
+
283
+ ```tsx
284
+ import { useNavigate } from '@tanstack/react-router'
285
+
286
+ function OpenPhotoButton({ photoId }: { photoId: string }) {
287
+ const navigate = useNavigate()
288
+
289
+ return (
290
+ <button
291
+ onClick={() =>
292
+ navigate({
293
+ to: '/photos/$photoId/modal',
294
+ params: { photoId },
295
+ mask: {
296
+ to: '/photos/$photoId',
297
+ params: { photoId },
298
+ },
299
+ })
300
+ }
301
+ >
302
+ Open Photo
303
+ </button>
304
+ )
305
+ }
306
+ ```
307
+
308
+ ### Declarative Masking with `createRouteMask`
309
+
310
+ ```tsx
311
+ import { createRouter, createRouteMask } from '@tanstack/react-router'
312
+ import { routeTree } from './routeTree.gen'
313
+
314
+ const photoModalMask = createRouteMask({
315
+ routeTree,
316
+ from: '/photos/$photoId/modal',
317
+ to: '/photos/$photoId',
318
+ params: (prev) => ({ photoId: prev.photoId }),
319
+ })
320
+
321
+ const router = createRouter({
322
+ routeTree,
323
+ routeMasks: [photoModalMask],
324
+ })
325
+ ```
326
+
327
+ ### Unmasking on Reload
328
+
329
+ By default, masks survive local page reloads. To unmask on reload:
330
+
331
+ ```tsx
332
+ // Per-mask
333
+ const mask = createRouteMask({
334
+ routeTree,
335
+ from: '/photos/$photoId/modal',
336
+ to: '/photos/$photoId',
337
+ params: (prev) => ({ photoId: prev.photoId }),
338
+ unmaskOnReload: true,
339
+ })
340
+
341
+ // Per-link
342
+ <Link
343
+ to="/photos/$photoId/modal"
344
+ params={{ photoId }}
345
+ mask={{ to: '/photos/$photoId', params: { photoId } }}
346
+ unmaskOnReload
347
+ >
348
+ Open Photo
349
+ </Link>
350
+
351
+ // Router-wide default
352
+ const router = createRouter({
353
+ routeTree,
354
+ unmaskOnReload: true,
355
+ })
356
+ ```
357
+
358
+ ## Common Mistakes
359
+
360
+ ### 1. HIGH: Using deprecated `NotFoundRoute`
361
+
362
+ ```tsx
363
+ // WRONG — NotFoundRoute blocks notFound() and notFoundComponent from working
364
+ import { NotFoundRoute } from '@tanstack/react-router'
365
+ const notFoundRoute = new NotFoundRoute({ component: () => <p>404</p> })
366
+ const router = createRouter({ routeTree, notFoundRoute })
367
+
368
+ // CORRECT — use notFoundComponent on root route
369
+ export const Route = createRootRoute({
370
+ component: () => <Outlet />,
371
+ notFoundComponent: () => <p>404</p>,
372
+ })
373
+ ```
374
+
375
+ ### 2. MEDIUM: Expecting `useLoaderData` in `notFoundComponent`
376
+
377
+ ```tsx
378
+ // WRONG — loader may not have completed
379
+ notFoundComponent: () => {
380
+ const data = Route.useLoaderData() // may be undefined!
381
+ return <p>{data.title} not found</p>
382
+ }
383
+
384
+ // CORRECT — use safe hooks
385
+ notFoundComponent: () => {
386
+ const { postId } = Route.useParams()
387
+ return <p>Post {postId} not found</p>
388
+ }
389
+ ```
390
+
391
+ ### 3. MEDIUM: Leaf routes cannot handle not-found errors
392
+
393
+ Only 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.
394
+
395
+ ```tsx
396
+ // This route has NO children — notFoundComponent here will not catch
397
+ // unmatched child paths (there are no child paths to unmatch)
398
+ export const Route = createFileRoute('/posts/$postId')({
399
+ // notFoundComponent here only works for notFound() thrown in THIS route's loader
400
+ // It does NOT catch path-based not-founds
401
+ notFoundComponent: () => <p>Not found</p>,
402
+ })
403
+ ```
404
+
405
+ ### 4. MEDIUM: Expecting masked URLs to survive sharing
406
+
407
+ 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.
408
+
409
+ ### 5. HIGH (cross-skill): Using `reset()` alone instead of `router.invalidate()`
410
+
411
+ ```tsx
412
+ // WRONG — reset() clears the error boundary but does NOT re-run the loader
413
+ function ErrorFallback({ error, reset }: { error: Error; reset: () => void }) {
414
+ return <button onClick={reset}>Retry</button>
415
+ }
416
+
417
+ // CORRECT — invalidate re-runs loaders and resets the error boundary
418
+ function ErrorFallback({ error }: { error: Error; reset: () => void }) {
419
+ const router = useRouter()
420
+ return (
421
+ <button
422
+ onClick={() => {
423
+ router.invalidate()
424
+ }}
425
+ >
426
+ Retry
427
+ </button>
428
+ )
429
+ }
430
+ ```
431
+
432
+ ## Cross-References
433
+
434
+ - **router-core/data-loading** — `notFound()` thrown in loaders interacts with error boundaries and loader data availability. `errorComponent` retry requires `router.invalidate()`.
435
+ - **router-core/type-safety** — `notFoundComponent` data is typed as `unknown`; validate before use.