not-found.js
Renders custom 404 UI when notFound() is thrown, and for unmatched URLs at the app root.
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.jsdoes double duty — it also catches every URL your app does not route at all, not just thrownnotFound()calls. - It takes no props. No
params, nosearchParams. To show context, readheaders()in an async component or move the work into a Client Component. - Streaming pins the status to 200. If a
loading.jsfallback 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. CallnotFound()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
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.
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.
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.
// 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