Lesson 36 of 81
notFound() isn't a redirect and it isn't a thrown error you write yourself — it's a specific signal Next.js recognizes anywhere in a Server Component's render path, and it hits a different rendering boundary than either of those.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
notFound() is a function you call from inside a Server Component — or any function that component awaits — to say "the thing this URL is asking for genuinely doesn't exist." Calling it throws a special NEXT_HTTP_ERROR_FALLBACK;404 error. Next.js catches that specific error as it propagates up the component tree and renders the nearest not-found.tsx in its place, the same mechanical idea as error.tsx catching a regular thrown error, just for a different signal.
It works in Server Components, Server Functions, and Route Handlers — not in Client Components, and not usefully inside a try/catch you wrote yourself, since a catch block will swallow the throw before Next.js ever sees it.
// app/products/[id]/page.tsx
import { notFound } from "next/navigation";
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`);
if (res.status === 404) {
notFound();
}
if (!res.ok) {
throw new Error(`Failed to load product: ${res.status}`);
}
return res.json();
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProduct(id);
return <h1>{product.name}</h1>;
}
// app/products/[id]/not-found.tsx
export default function ProductNotFound() {
return <p>We couldn't find that product. It may have been discontinued.</p>;
}
getProduct is the natural place for the check — it's the function that actually knows the resource is missing, and it's awaited directly by the component, so the throw travels straight up into React's render and gets caught by the not-found.tsx sitting next to the page. No if (!product) return <SomethingElse /> needed anywhere.
getProduct is called without await, the throw happens inside a promise nobody's listening to — no not-found UI renders, and in development the server just logs an unhandled rejection. The call has to happen in the actual render path: a component body, or a function that component awaits.notFound() explicitly renders the nearest not-found.tsx up the tree from wherever it was called. Landing on a URL that matches no route at all is a separate case, handled by the root app/not-found.tsx (or the experimental global-not-found.tsx, which bypasses your layout entirely and needs its own <html>/<body>) — a nested not-found.tsx never catches a plain unmatched URL, only an explicit call within its own subtree.<Suspense> — so the page shell can ship immediately — the response has already begun as a 200 by the time notFound() fires. Next.js still swaps in the not-found UI and adds a noindex meta tag, but it can't rewrite headers that already left the server. A real 404 status requires the check to happen before anything streams, which usually means checking it in proxy.ts instead if the shell itself needs to stream.try/catch around the call defeats it, same as with redirect(). Both work by throwing a specific error Next.js's runtime recognizes; a surrounding catch intercepts it like any other exception before the framework gets a chance to act on it.global-not-found.tsx is a different tool for a different shape of app, still experimental behind a config flag — it's for when there's no single layout to compose a 404 from at all (multiple root layouts, or a root layout built on a dynamic segment), not a drop-in replacement for the ordinary not-found.tsx.The lesson on loading, error, and not-found UI conventions covers not-found.tsx as one of four sibling files wired up by naming alone. This lesson is the deep dive on the other half of that pair — the notFound() function itself, its two distinct trigger paths, and the streaming status-code trade-off that only shows up once you actually call it from inside a data-fetching function rather than just reading about the file convention.