not-found.js

Renders custom 404 UI when notFound() is thrown, and for unmatched URLs at the app root.

Signature
export default function NotFound(): React.ReactNode
  • Added in Next.js v13.0.0

Return value

React.ReactNode | Promise<React.ReactNode>The UI rendered in place of the segment when notFound() is thrown, or for an unmatched URL when the file is at the app root.

Changelog

  • PHP v15.4.0global-not-found.js introduced (experimental, behind the globalNotFound flag) for apps that cannot compose a 404 from layout.js plus not-found.js.
  • PHP v13.3.0Root app/not-found now also handles globally unmatched URLs.
  • PHP v13.0.0not-found introduced.

A not-found file renders when notFound() is thrown inside its route segment. Next.js returns 404 for non-streamed responses and 200 for streamed ones. In the component hierarchy it sits between loading.js and page.js, so the segment's Suspense and error boundaries still wrap it.

Use it wherever a URL is well-formed but the record behind it is not there: /blog/does-not-exist, a deleted product, an archived ticket. The copy of a good 404 is segment-specific, which is the point of having more than one.

What trips people up

  • The root app/not-found.js does double duty — it also catches every URL your app does not route at all, not just thrown notFound() calls.
  • It takes no props. No params, no searchParams. To show context, read headers() in an async component or move the work into a Client Component.
  • Streaming pins the status to 200. If a loading.js fallback has already rendered, headers are sent and the 404 status is gone — Next.js adds <meta name="robots" content="noindex"> so it is not indexed. Call notFound() before anything suspends if you need the real status.
  • The default UI follows the OS colour scheme, not your app theme, because it renders inside your root layout without your theme hooks.
  • Multiple root layouts? Look at experimental global-not-found.js.

Example

Example · typescript
import Link from 'next/link'

export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
      <Link href="/">Return Home</Link>
    </div>
  )
}

When to use it

  • Show a blog-specific 404 with a link back to the archive when a slug does not exist
  • Catch every unrouted URL in the app with a single root app/not-found.tsx
  • Render 404 UI after a database lookup returns nothing for a dynamic segment
  • Give a tenant-aware message by reading the host header in an async not-found component

Frequently asked questions

Why does my not-found page return a 200 instead of 404?

Because the response had already started streaming when notFound() was thrown — typically a loading.js fallback rendered first — and the status cannot change once headers are sent. Next.js compensates by injecting <meta name="robots" content="noindex">, so the URL is not indexed. To get a real 404, do the existence check before any await that suspends.

How do I get params in not-found.js?

You cannot: not-found.js components accept no props at all. Read request context instead by marking the component async and calling headers() from next/headers, or pass what you need through a Client Component that uses usePathname. If you need the failing identifier in the copy, format it in the page before calling notFound().

What is the difference between not-found.js and global-not-found.js?

not-found.js works at the route level: it renders when notFound() is thrown in a segment, and the root copy also covers unmatched URLs. global-not-found.js is experimental (v15.4.0, behind the globalNotFound flag) and handles URLs that match no route at all by skipping rendering entirely, so it must return a full HTML document. Reach for it when multiple root layouts or a top-level dynamic segment prevent composing a 404 from layout.js and not-found.js.

More examples

Throwing notFound() from a page

notFound() throws, so no return statement is needed and everything after it is unreachable — TypeScript narrows post to non-null below the guard. The throw is caught by the nearest not-found.js, here app/blog/not-found.tsx. Because it runs before anything suspends, the response can still carry a real 404 status.

Example · typescript
import { notFound } from 'next/navigation'
import { getPost } from '@/app/lib/posts'

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)

  if (!post) {
    notFound()
  }

  return <article>{post.title}</article>
}

Reading request context in an async not-found

not-found.js is a Server Component and can be marked async, which is the way around it receiving no props: the host header identifies the tenant on a multi-domain deployment. headers() is async in Next.js 15+, so it must be awaited. Client hooks such as usePathname are unavailable here — that work belongs in a Client Component child.

Example · typescript
import Link from 'next/link'
import { headers } from 'next/headers'
import { getSiteData } from '@/app/lib/sites'

export default async function NotFound() {
  const headersList = await headers()
  const domain = headersList.get('host')
  const data = await getSiteData(domain)

  return (
    <div>
      <h2>Not Found: {data.name}</h2>
      <p>Could not find requested resource</p>
      <p>
        View <Link href="/blog">all posts</Link>
      </p>
    </div>
  )
}

An app-wide 404 for multiple root layouts

This is app/global-not-found.tsx, experimental since v15.4.0 and enabled with experimental.globalNotFound in next.config.ts. It bypasses rendering entirely for unmatched URLs, so — like global-error — it must supply its own <html>, <body>, styles and fonts. Use it when several root layouts or a top-level dynamic segment make a single composed 404 impossible.

Example · typescript
// Import global styles and fonts
import './globals.css'
import { Inter } from 'next/font/google'
import type { Metadata } from 'next'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: '404 - Page Not Found',
  description: 'The page you are looking for does not exist.',
}

export default function GlobalNotFound() {
  return (
    <html lang="en" className={inter.className}>
      <body>
        <h1>404 - Page Not Found</h1>
        <p>This page does not exist.</p>
      </body>
    </html>
  )
}

Discussion

  • Be the first to comment on this lesson.