Skip to the solution
EasyFrontend9 min read

What is not-found.js in Next.js and how do you trigger a 404?

840 views

The problem

Question presented to candidate: "A user opens /products/999 and that product does not exist. How do you show a proper 'not found' page in the App Router, and how do you make sure search engines treat it as a 404?"

What a strong answer should cover:

  • Call notFound() from next/navigation in a Server Component, Server Function or Route Handler when the data is missing; it throws, so no return is needed and TypeScript narrows the value after it.
  • Next.js renders the nearest not-found.tsx up the tree from where it was thrown; the root app/not-found.tsx also handles every URL that matches no route.
  • Before streaming starts the response is a real 404; after a loading.tsx or Suspense fallback has streamed, the status stays 200, and in both cases a noindex robots tag is injected.
  • A try/catch around the call swallows it; rethrow with unstable_rethrow.
  • not-found.tsx takes no props, is a Server Component by default and can be async; global-not-found.tsx (experimental) covers apps with several root layouts.
  • Related: forbidden() and unauthorized() work the same way for 403 and 401.

Clarifying questions expected:

  • "Does the exact status code matter for analytics or compliance?" β€” then check before anything streams, or reject in Proxy.
  • "Should a missing product show a product-specific message?" β€” then add a not-found.tsx inside that segment.

Code / implementation expected: Yes β€” a data function that calls notFound(), root and segment not-found.tsx files, and a page that uses unstable_rethrow.

  • nextjs
  • not-found
  • 404
  • routing

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 8 sections

Read the code

Data-level notFound(), root and segment not-found files, and unstable_rethrow
// app/lib/products.ts
import { notFound } from "next/navigation";

const products: Record<string, { name: string }> = { "1": { name: "Keyboard" } };

// The idiomatic place for the existence check: the data function itself.
export async function getProduct(id: string) {
  const product = products[id];
  if (!product) notFound(); // throws, so TypeScript knows product is defined below
  return product;
}

// app/not-found.tsx
import Link from "next/link";

// Root not-found: unmatched URLs anywhere in the app, and notFound() calls with no closer file.
export default function NotFound() {
  return (
    <main>
      <h1>Page not found</h1>
      <Link href="/">Back home</Link>
    </main>
  );
}

// app/products/not-found.tsx
// Closer file: used when notFound() is called inside /products/*.
export default function ProductNotFound() {
  return <p>That product does not exist. Try the catalogue.</p>;
}

// app/products/[id]/page.tsx
import type { Metadata } from "next";
import { unstable_rethrow } from "next/navigation";
import { getProduct } from "../../lib/products";

export async function generateMetadata({ params }: PageProps<"/products/[id]">): Promise<Metadata> {
  const { name } = await getProduct((await params).id); // notFound() is allowed in generateMetadata too
  return { title: name };
}

export default async function ProductPage({ params }: PageProps<"/products/[id]">) {
  const { id } = await params;
  try {
    const product = await getProduct(id);
    return <h1>{product.name}</h1>;
  } catch (err) {
    unstable_rethrow(err); // let notFound() through; a bare catch would swallow the 404
    return <p>Could not load the product.</p>;
  }
}

// app/api/products/[id]/route.ts
import { getProduct } from "../../../lib/products";

// In a Route Handler, notFound() produced an empty 404 in the lab.
export async function GET(_req: Request, ctx: RouteContext<"/api/products/[id]">) {
  const { id } = await ctx.params;
  return Response.json(await getProduct(id));
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 9 of 129 in Next.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Next.js questions
Up nextEasy

What is the next/form Form component and when would you use it?

Continue