notFound()
Throws a 404 interrupt that renders the nearest not-found.js boundary and stops the route segment.
notFound(): never- Added in Next.js v13.0.0
Return value
neverDoes not return a value. It throws a NEXT_HTTP_ERROR_FALLBACK;404 error and terminates rendering of the route segment where it was called.
Errors
Always throws NEXT_HTTP_ERROR_FALLBACK;404 by design; a try/catch around the call suppresses it and the not-found UI never renders.
Changelog
- PHP v13.0.0notFound introduced.
Call notFound() when a dynamic segment resolves to nothing: an id with no row, a slug that was deleted, an API responding 404. Next.js throws NEXT_HTTP_ERROR_FALLBACK;404, unwinds the segment, renders the nearest not-found.js boundary (falling back to the built-in 404 page), and injects <meta name="robots" content="noindex" /> so the page is kept out of search results.
Because it throws, it must run in the render path — a component, or a function a component awaits. A call left inside an un-awaited promise throws where nothing is listening; in development you see ⨯ unhandledRejection: NEXT_HTTP_ERROR_FALLBACK;404 and no 404 UI at all.
The status code is not guaranteed
You only get a real 404 status if the check runs before the response starts streaming. Do the check inside a <Suspense> boundary — or under Cache Components, where every dynamic route streams a static shell first — and the headers are already out as 200. The UI still renders and the noindex tag still applies; it is a soft 404. For a hard status, check in proxy.
No return is needed: the never type keeps a checked value narrowed afterwards.
Example
import { notFound } from 'next/navigation'
async function fetchUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`)
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()
}
return <h1>{user.name}</h1>
}When to use it
- Show the 404 UI when a database lookup for a dynamic [id] returns nothing
- Turn an upstream API 404 into the site's own not-found page inside a data-access function
- Serve a 404 from a Route Handler when the requested resource does not exist
- Reject a dynamic segment whose value is not in the allowed set
Frequently asked questions
Why does notFound() return a 200 status instead of 404?
Because the check ran after the response started streaming — typically inside a Suspense boundary, or on a dynamic route that streams a static shell first. The status is fixed once headers are sent; move the check ahead of streaming, or into proxy, to get a real 404.
Why is my not-found.js not rendering?
Most often a try/catch around the call is swallowing the NEXT_HTTP_ERROR_FALLBACK;404 error. Call unstable_rethrow(err) at the top of the catch block, or move the call out of the try. The other cause is a notFound() sitting in a promise nobody awaited.
Do I need to write return notFound()?
No. notFound() is typed as never and throws, so execution stops at the call and TypeScript treats everything after it as unreachable — which also means a value you null-checked just above stays narrowed below.
More examples
Putting the check in the data-access function
The page shell and loading fallback stream immediately while the post loads. When getPost throws, the exception propagates out of the Suspense boundary to the nearest not-found boundary, which replaces the streamed-in content. The trade-off is visible in the network tab: the response is already a 200, so this is a soft 404 rescued only by the injected noindex tag.
// app/blog/[slug]/page.tsx
import { Suspense } from 'react'
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,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return (
<Suspense fallback={<p>Loading...</p>}>
<Article slug={slug} />
</Suspense>
)
}The matching not-found.tsx boundary
Placing not-found.tsx beside the route scopes this UI to /blog/[slug] and anything below it. Without this file the throw keeps travelling up to the nearest parent boundary, and if there is none, Next.js renders its default 404 page instead.
// app/blog/[slug]/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<section>
<h1>Post not found</h1>
<p>The post you are looking for does not exist.</p>
<Link href="/blog">Back to the blog</Link>
</section>
)
}Serving a 404 from a Route Handler
In a Route Handler there is no UI boundary to render, so the throw is turned into a plain 404 response to the caller. Nothing is streaming yet at the point of the call, so this one does produce a real 404 status rather than a soft one.
// app/api/posts/[slug]/route.ts
import { NextResponse } from 'next/server'
import { notFound } from 'next/navigation'
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) {
notFound()
}
return NextResponse.json(await res.json())
}
Discussion