Type something to search...
Next.js not-found.js

Next.js not-found.js

Next.js actually gives you two distinct tools for "this doesn't exist" scenarios, and conflating them is the most common mistake people make with this convention. not-found.js handles a resource your own code determined doesn't exist — you called notFound() deliberately, inside a route segment, after checking a database or an ID that didn't resolve. global-not-found.js handles something structurally different: a URL that doesn't match any route in your app at all, resolved entirely at the routing layer, before your rendering code even gets involved.

Both matter, and they solve genuinely different problems — this article covers each in turn.

not-found.js: For Explicit, Code-Triggered Not-Found States

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>
  );
}

This file renders whenever the notFound() function is invoked within a route segment — typically after a data lookup comes back empty, and your code decides that means the requested content genuinely doesn't exist. Along with the custom UI, Next.js handles the status code correctly depending on how the response is delivered: 200 for streamed responses (with a noindex marker injected, as covered in the streaming/loading.js reference), and 404 for non-streamed responses.

Where It Sits in the Hierarchy

In the component hierarchy, not-found.js renders between loading.js and page.js — meaning it's wrapped by the <Suspense> boundary loading.js establishes, and by the error boundary error.js provides at the same segment. Practically: if something goes wrong while rendering your not-found UI itself, that's still caught by the same error.js covering everything else in the segment; it isn't a special exemption.

Theming the Default UI

The built-in default not-found UI follows the operating system's color scheme via prefers-color-scheme — it does not read an app-level theme mechanism like a data-theme attribute or class on <html>. Because it still renders inside your root layout (unlike global-not-found.js, discussed below), the fastest fix if you have an explicit light/dark toggle is a pair of higher-specificity CSS rules scoped to your theme selector:

html[data-theme="light"] body {
  /* light-theme not-found overrides */
}
html[data-theme="dark"] body {
  /* dark-theme not-found overrides */
}

For full control over the markup itself rather than just theming, define your own not-found.js — which is what most real applications end up doing anyway, since the default UI is deliberately minimal.

global-not-found.js: For Genuinely Unmatched Routes (Experimental)

This is the newer, structurally different convention. Where not-found.js fires from inside your rendering tree in response to your own notFound() call, global-not-found.js intercepts requests that don't match any route in your application at all — Next.js skips rendering entirely and returns this file directly, at the routing layer.

Why You'd Need It

global-not-found.js exists specifically for two situations where composing a consistent 404 experience from layout.js + not-found.js genuinely doesn't work:

  • Multiple root layouts — if your app has separate root layouts like app/(admin)/layout.tsx and app/(shop)/layout.tsx, there's no single shared layout left to hang a consistent 404 page off of.
  • Root layouts under a dynamic segment — internationalized routing patterns like app/[country]/layout.tsx make it structurally awkward to define one coherent not-found experience, since the root layout itself depends on a param that, by definition, an unmatched route can't supply.

Enabling It

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    globalNotFound: true,
  },
};

export default nextConfig;

Then create app/global-not-found.tsx directly at the app root:

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>
  );
}

The Critical Difference: This File Bypasses Your App Entirely

This is the detail that will bite you if you skim past it: global-not-found.js bypasses your app's normal rendering, which means you must manually import every global dependency your page needs — global stylesheets, fonts, and (unlike ordinary not-found.js) your theme itself. Because it doesn't render inside your root layout at all, the OS color scheme is the only signal the default UI would otherwise see; if you want your explicit theme applied here, you have to bring that class or attribute logic into this file directly rather than relying on it inheriting from anywhere else.

The docs specifically suggest a lighter version of your global styles and a simpler font stack for this page — since it's on the critical path for every genuinely broken link hitting your site, keeping it lean is a meaningful performance consideration, not just a nice-to-have.

Also worth internalizing: unlike ordinary not-found.js, this file must return a complete HTML document, with its own <html> and <body> tags — there's no ambient layout supplying those for you.

Props: Neither File Accepts Any

Both not-found.js and global-not-found.js are prop-free components. Beyond catching explicit notFound() calls, the root-level app/not-found.js (and app/global-not-found.js, if enabled) also serves as the catch-all for any unmatched URL across your entire application — meaning visitors hitting a URL your app simply doesn't handle will see whichever of these two you've configured.

Fetching Data Inside not-found.js

By default, not-found is a Server Component, so it can be marked async and fetch data just like any other Server Component:

import Link from "next/link";
import { headers } from "next/headers";

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>
  );
}

This is genuinely useful for multi-tenant setups — reading the request's host to customize the not-found message per tenant, for instance. One limitation worth knowing: if you need Client Component hooks like usePathname to tailor content based on the current path, you can't do that here directly, since not-found.js in its async, data-fetching form is a Server Component — you'd need to fetch that path-dependent content client-side instead.

Metadata on global-not-found.js

Because global-not-found.js renders as a genuine, standalone document, it supports the full Metadata API — a plain metadata object or a generateMetadata function, exactly like any other route:

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Not Found",
  description: "The page you are looking for does not exist.",
};

export default function GlobalNotFound() {
  return (
    <html lang="en">
      <body>
        <div>
          <h1>Not Found</h1>
          <p>The page you are looking for does not exist.</p>
        </div>
      </body>
    </html>
  );
}

Next.js automatically injects a <meta name="robots" content="noindex" /> tag for any page returning a 404 status, global-not-found.js included — you don't need to add that yourself.

Version History

VersionChanges
v15.4.0global-not-found.js introduced (experimental)
v13.3.0Root app/not-found began handling global unmatched URLs
v13.0.0not-found introduced

Key Takeaways

not-found.jsglobal-not-found.js
Triggered byExplicit notFound() call in your codeAny URL matching no route at all
Renders inside your layout?YesNo — bypasses your app entirely
Must include <html>/<body>?No (inherited from layout)Yes — must be a full document
ThemingFollows OS scheme; override via layout-scoped CSSMust apply your theme logic manually — no ambient layout to inherit from
Metadata supportN/A (not a route in the usual sense)Full Metadata API support
StatusStableExperimental
When you need itAlmost always — the default 404 handler for your appOnly with multiple root layouts, or a root layout under a dynamic segment

For most apps, ordinary not-found.js is all you'll ever touch. Reach for global-not-found.js specifically when your root-layout structure makes a single, consistent 404 page impossible to compose the normal way — and when you do, remember it's an island unto itself, responsible for its own styles, fonts, theme, and document shell.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper