Files
Aurora ScharffandGitHub 0be0ebe5fc docs: navigation interrupts after streaming has started (notFound, forbidden, unauthorized) (#95851)
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.
2026-07-24 14:56:01 +02:00

235 lines
7.6 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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. |