Saying 404 on purpose
Suppose /blog/does-not-exist is requested and your fetch finds no post. Rendering an empty page is misleading, and throwing an error would show the error boundary, which suggests something broke on your side. The honest answer is a 404, and Next.js gives you a function for it:
import { notFound } from "next/navigation"; export default async function PostPage({ params }) { const { slug } = await params; const post = await getPost(slug); if (!post) notFound(); return <h1>{post.title}</h1>; }
Calling notFound() stops rendering and shows the nearest not-found.js file, which you write like any other special file:
// app/blog/not-found.js export default function NotFound() { return <p>That post does not exist.</p>; }
The response also carries a real HTTP 404 status, which search engines respect. You know why that matters from Backend with Node.js: status codes are the contract of the web.
What notFound() actually does
Calling notFound() from a server component has three effects at once. Rendering stops immediately, the nearest not-found.js in the folder tree renders in place of the page, and the response carries a genuine HTTP 404 status.
All three matter. Aborting the render means the rest of your component never runs against missing data, so you avoid a cascade of "cannot read property of undefined" errors. Rendering the closest not-found.js means a blog can show a blog-flavoured message while the rest of the site keeps its own. The real status code is the signal both users and crawlers need, since a "not found" page served with a 200 status tells search engines the URL is valid content.
Catch-all segments
One more bracket trick. [slug] matches exactly one segment. Add three dots and it matches any depth:
app/docs/[...parts]/page.js
/docs/introgivesparts === ["intro"]/docs/api/auth/tokensgivesparts === ["api", "auth", "tokens"]
The param becomes an array of segments rather than a single string. Catch-alls power docs sites and wikis where the hierarchy lives in data instead of in folders.
Use them sparingly. If the depth of your URLs is fixed, explicit folders are clearer and give you better type safety and error messages.
Counting segments in a catch-all
With app/docs/[...parts]/page.js in place, a user visits /docs/guides/routing. The parts array holds two strings: ["guides", "routing"].
Why it works out that way
- A catch-all collects every remaining segment into an array, with one string per segment, so two segments after
/docsproduce two elements. - The order matches the URL left to right, which means
parts[0]is always the segment nearest the top of the hierarchy. - Nothing is coerced or joined for you. If you want the original path back, you join the array yourself with
parts.join("/").