not-found and Redirects

not-found.js, global-error.js, and Redirects

notFound() from next/navigation throws a special error that the nearest not-found.js catches; the root app/not-found.js does double duty as the page for any URL matching no route at all. Probing the running server with curl 3,008 shows what each convention returns:

Output of 17
/                200
/nope            404
/blog/missing    404
/old-blog        308  ->  location: /blog
/broken          500

/blog/missing is a real 404 because its page validates the slug and calls notFound() before any await that suspends. Call it after one, or below a <Suspense> boundary, and the status is already committed to 200; Next.js 10,514 then injects <meta name="robots" content="noindex"> so the URL is not indexed anyway. If you need the status code, do a cheap existence check before any boundary, or reject the request in proxy.ts (proxy.ts).

global-error.js is the boundary of last resort: it catches throws from the root layout, and because it replaces that layout it must render its own <html> and <body>, import its own styles and fonts, and do without a metadata export — use React 7,897 's <title> element instead. Its experimental sibling global-not-found.js, behind experimental.globalNotFound, returns a complete document for unmatched URLs without rendering any layout, which is what an app with several root layouts needs.

Redirects come in four flavors, and the wrong one costs a render or a round trip.

Where each kind of redirect happens and what it costs
Mechanism Runs Status Use for
redirects in next.config Before routing 307 / 308 Known, static URL moves
proxy.ts Before rendering Any you set Auth checks, locale, A/B splits
redirect() While rendering 307, or 303 in a form post "This record moved"
router.push() In the browser none After a client-side event

redirect(path, type) replaces the current history entry by default and pushes a new one inside a Server Action; permanentRedirect() is the 308 version, which is what /old-blog uses above. Both work by throwing a NEXT_REDIRECT error, so a try/catch around the call swallows the redirect — always call them outside the try block. They need no return, they accept absolute URLs, and once streaming has begun they degrade to a <meta> tag emitted into the stream rather than an HTTP status.