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:
/ 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.
| 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.