notFound()

Throws a 404 interrupt that renders the nearest not-found.js boundary and stops the route segment.

Signature
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

Example · typescript
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.

Example · typescript
// 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.

Example · typescript
// 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.

Example · typescript
// 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

  • Be the first to comment on this lesson.