mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Adds a "Calling `x()` after streaming has started" example to the `notFound`, `forbidden`, and `unauthorized` references. - The check lives in the data-access function that loads the data, inside a `<Suspense>` boundary, so the page shell keeps streaming (blog post 404, account 401, admin projects 403). - Documents the status-code trade-off: the interrupt fires after headers are sent, so the response is a `200`; a `loading.tsx` is part of the stream, not a way around it; `instant = false` opts the route out of the shell for a real status code. - Links the authentication guide's DAL section instead of re-teaching it, plus a good-to-know on floating promises never rendering the UI. Docs-only.
235 lines
7.6 KiB
Plaintext
235 lines
7.6 KiB
Plaintext
---
|
||
title: notFound
|
||
description: API Reference for the notFound function.
|
||
related:
|
||
links:
|
||
- app/api-reference/file-conventions/not-found
|
||
- app/api-reference/functions/forbidden
|
||
- app/api-reference/functions/unauthorized
|
||
---
|
||
|
||
The `notFound` function throws an error that renders a Next.js 404 page. It's useful for handling missing resources in your application. You can customize the UI using the [`not-found.js` file](/docs/app/api-reference/file-conventions/not-found).
|
||
|
||
Invoking `notFound()` throws a `NEXT_HTTP_ERROR_FALLBACK;404` error and terminates rendering of the route segment where it was thrown. Next.js also injects a `<meta name="robots" content="noindex" />` tag so the page is not indexed. Because it works by throwing, call it in the render path: a component, or a function a component `await`s. A call left in an un-awaited promise throws where nothing catches it, and no not-found UI renders (in development the server logs `⨯ unhandledRejection: NEXT_HTTP_ERROR_FALLBACK;404`).
|
||
|
||
`notFound()` can be invoked in [Server Components](/docs/app/getting-started/server-and-client-components), [Server Functions](/docs/app/getting-started/mutating-data), and [Route Handlers](/docs/app/api-reference/file-conventions/route).
|
||
|
||
```tsx filename="app/user/[id]/page.tsx" switcher
|
||
import { notFound } from 'next/navigation'
|
||
|
||
async function fetchUser(id: string) {
|
||
const res = await fetch('https://...')
|
||
if (!res.ok) return undefined
|
||
return res.json()
|
||
}
|
||
|
||
export default async function Profile({
|
||
params,
|
||
}: {
|
||
params: Promise<{ id: string }>
|
||
}) {
|
||
const { id } = await params
|
||
const user = await fetchUser(id)
|
||
|
||
if (!user) {
|
||
notFound()
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
```jsx filename="app/user/[id]/page.js" switcher
|
||
import { notFound } from 'next/navigation'
|
||
|
||
async function fetchUser(id) {
|
||
const res = await fetch('https://...')
|
||
if (!res.ok) return undefined
|
||
return res.json()
|
||
}
|
||
|
||
export default async function Profile({ params }) {
|
||
const { id } = await params
|
||
const user = await fetchUser(id)
|
||
|
||
if (!user) {
|
||
notFound()
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## Good to know
|
||
|
||
You do not need to write `return notFound()`. Calling it is enough, because it throws an exception that stops function execution. TypeScript understands this from its [`never`](https://www.typescriptlang.org/docs/handbook/2/functions.html#never) return type, so a value you check first stays narrowed afterward:
|
||
|
||
```tsx
|
||
// fetchUser resolves to a user object, or undefined
|
||
const user = await fetchUser(id)
|
||
|
||
if (!user) {
|
||
notFound()
|
||
}
|
||
|
||
// user is defined here
|
||
return <Profile user={user} />
|
||
```
|
||
|
||
Like any exception, it travels up the call stack until something catches it. A `try/catch` around the call suppresses it, and the not-found UI won't render. If you need to catch errors near the call, use [`unstable_rethrow`](/docs/app/api-reference/functions/unstable_rethrow) to let the interrupt through first.
|
||
|
||
## Examples
|
||
|
||
### Calling `notFound()` after streaming has started
|
||
|
||
To keep a page's shell and loading UI visible while data loads, do the existence check inside a component wrapped in [`<Suspense>`](https://react.dev/reference/react/Suspense) instead of blocking the whole route. The idiomatic place for the check is the data-access function itself, awaited by the component that needs the data:
|
||
|
||
```tsx filename="app/blog/[slug]/page.tsx" switcher highlight={8}
|
||
import { Suspense } from 'react'
|
||
import Link from 'next/link'
|
||
import { notFound } from 'next/navigation'
|
||
|
||
async function getPost(slug: string) {
|
||
const res = await fetch(`https://api.example.com/posts/${slug}`)
|
||
if (res.status === 404) {
|
||
notFound()
|
||
}
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to load post: ${res.status}`)
|
||
}
|
||
return res.json()
|
||
}
|
||
|
||
async function Article({ slug }: { slug: string }) {
|
||
const post = await getPost(slug)
|
||
return (
|
||
<article>
|
||
<h1>{post.title}</h1>
|
||
<p>{post.content}</p>
|
||
</article>
|
||
)
|
||
}
|
||
|
||
export default async function PostPage({ params }: PageProps<'/blog/[slug]'>) {
|
||
const { slug } = await params
|
||
|
||
return (
|
||
<section>
|
||
<Link href="/blog">Blog</Link>
|
||
<Suspense fallback={<p>Loading...</p>}>
|
||
<Article slug={slug} />
|
||
</Suspense>
|
||
</section>
|
||
)
|
||
}
|
||
```
|
||
|
||
```jsx filename="app/blog/[slug]/page.js" switcher highlight={8}
|
||
import { Suspense } from 'react'
|
||
import Link from 'next/link'
|
||
import { notFound } from 'next/navigation'
|
||
|
||
async function getPost(slug) {
|
||
const res = await fetch(`https://api.example.com/posts/${slug}`)
|
||
if (res.status === 404) {
|
||
notFound()
|
||
}
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to load post: ${res.status}`)
|
||
}
|
||
return res.json()
|
||
}
|
||
|
||
async function Article({ slug }) {
|
||
const post = await getPost(slug)
|
||
return (
|
||
<article>
|
||
<h1>{post.title}</h1>
|
||
<p>{post.content}</p>
|
||
</article>
|
||
)
|
||
}
|
||
|
||
export default async function PostPage({ params }) {
|
||
const { slug } = await params
|
||
|
||
return (
|
||
<section>
|
||
<Link href="/blog">Blog</Link>
|
||
<Suspense fallback={<p>Loading...</p>}>
|
||
<Article slug={slug} />
|
||
</Suspense>
|
||
</section>
|
||
)
|
||
}
|
||
```
|
||
|
||
When the post doesn't exist, `getPost` calls `notFound()`, which throws. Because this happens during rendering, the exception propagates to the nearest [`not-found`](/docs/app/api-reference/file-conventions/not-found) boundary, which renders in place of the streamed-in content, even though the page shell has already been sent.
|
||
|
||
Add a `not-found.tsx` alongside the route to define that UI. Without one, the nearest parent `not-found` boundary renders, falling back to Next.js's default 404 page:
|
||
|
||
```tsx filename="app/blog/[slug]/not-found.tsx" switcher
|
||
export default function NotFound() {
|
||
return (
|
||
<section>
|
||
<h1>Post not found</h1>
|
||
<p>The post you're looking for doesn't exist.</p>
|
||
</section>
|
||
)
|
||
}
|
||
```
|
||
|
||
```jsx filename="app/blog/[slug]/not-found.js" switcher
|
||
export default function NotFound() {
|
||
return (
|
||
<section>
|
||
<h1>Post not found</h1>
|
||
<p>The post you're looking for doesn't exist.</p>
|
||
</section>
|
||
)
|
||
}
|
||
```
|
||
|
||
The trade-off is the HTTP status code. Because the check runs inside the `<Suspense>` boundary, the response has already begun streaming as a `200`, and the status can't change once streaming has started. The `noindex` tag keeps a soft 404 out of search results. To return a real `404` status, the resource has to be checked before the response streams. With [Cache Components](/docs/app/getting-started/caching), every dynamic route streams a static shell first, so run that check in [`proxy`](/docs/app/api-reference/file-conventions/proxy) instead. See [Status codes](/docs/app/api-reference/file-conventions/loading#status-codes).
|
||
|
||
### Serving a 404 from a Route Handler
|
||
|
||
`notFound()` also works in a [Route Handler](/docs/app/api-reference/file-conventions/route), where it serves a `404` to the caller.
|
||
|
||
```tsx filename="app/api/posts/[slug]/route.ts" switcher
|
||
import { NextResponse } from 'next/server'
|
||
import { notFound } from 'next/navigation'
|
||
|
||
export async function GET(
|
||
request: Request,
|
||
{ params }: RouteContext<'/api/posts/[slug]'>
|
||
) {
|
||
const { slug } = await params
|
||
const res = await fetch(`https://api.example.com/posts/${slug}`)
|
||
if (!res.ok) {
|
||
notFound()
|
||
}
|
||
return NextResponse.json(await res.json())
|
||
}
|
||
```
|
||
|
||
```jsx filename="app/api/posts/[slug]/route.js" switcher
|
||
import { NextResponse } from 'next/server'
|
||
import { notFound } from 'next/navigation'
|
||
|
||
export async function GET(request, { params }) {
|
||
const { slug } = await params
|
||
const res = await fetch(`https://api.example.com/posts/${slug}`)
|
||
if (!res.ok) {
|
||
notFound()
|
||
}
|
||
return NextResponse.json(await res.json())
|
||
}
|
||
```
|
||
|
||
## Version History
|
||
|
||
| Version | Changes |
|
||
| --------- | ---------------------- |
|
||
| `v13.0.0` | `notFound` introduced. |
|