skip to content

A Next.js App Router project has app/blog/not-found.tsx, but visiting /blog/does-not-exist shows the application's root 404 page instead of that file. Why, and when does a nested not-found.tsx actually render?

level: middleimportance: should knowfreq 42%

answer

  1. two different kinds of 404
  2. did routing ever enter that folder
  3. unmatched URLs have no segment context
  4. the file shows, it does not match
  5. status code matters more than the markup

basics

~20 s

A URL that matches no route at all is always handled by the root not-found file, because routing never reaches the blog segment. A nested not-found.tsx renders only when the not-found helper is thrown while that subtree is being rendered — typically by a page that looked up a missing record.

solid answer

~50 s

There are two different "404" situations and they are handled by different files. `/blog/does-not-exist` matches no route in the tree — unless `app/blog/` contains a dynamic segment, there is nothing under `blog` that accepts that path — so routing never enters the blog subtree at all. Unmatched URLs for the whole application are served by the root `app/not-found.tsx`, which is why you see the app-wide page. A nested `not-found.tsx` covers the *other* case: a route that matched fine but whose data does not exist, where the segment calls Next's `notFound()` helper during render. Next then walks up to the closest `not-found.tsx` above the throwing component and renders it with a 404 status. So the fix is a dynamic segment under `app/blog/` whose page calls `notFound()` when the post is missing; then `app/blog/not-found.tsx` renders as intended.

go deeper

for a junior

Know that a not-found file renders a 404 page, that the root one covers URLs matching no route, and that it receives no props because nothing has failed.

for a middle

Distinguish the unmatched-URL case from the matched-route-missing-resource case, and explain that a nested file only renders once routing has entered that subtree and the page declares the resource missing.

for a senior

Show why the 404 status matters for crawlers and monitoring, and keep the taxonomy clean in review — missing rows are not-found, failed dependencies are errors.

for a principal

Own how missing resources are represented across the product: which areas deserve their own not-found UI, what those pages link to for recovery, and how 404 rates are monitored as a signal rather than noise.

## Two different 404s The confusion here is that "404" covers two situations the App Router treats separately: 1. **The URL matches no route.** Nothing in the segment tree accepts `/blog/does-not-exist`. Routing fails before any of your code runs. 2. **The route matched but the resource does not exist.** `/blog/some-slug` resolved to a real segment, the page ran, the lookup returned nothing, and the code deliberately declares the resource missing. Case 1 is application-wide by nature. There is no "current segment" to be in — Next never entered one — so the only file that can answer is the **root** `app/not-found.tsx`. That is the file that handles any unmatched URL for the whole application, and it is why a nested `not-found.tsx` appears to be ignored. Case 2 is the one nested files exist for. Because the route matched, there *is* a position in the tree, and Next can pick the closest `not-found.tsx` at or above that position. ## Why the blog example never reaches the blog subtree Spell out what the folder actually declares: ``` app/ not-found.tsx root: handles all unmatched URLs blog/ page.tsx /blog not-found.tsx only reachable when routing entered this subtree ``` The only URL this tree accepts under `blog` is `/blog` itself. `/blog/does-not-exist` needs a segment able to accept an extra path piece; there is none, so the match fails outright and the root file answers. Adding a `not-found.tsx` deeper in the tree cannot change matching — that file describes what to show *after* a match, not which paths are accepted. Once a dynamic segment exists under `blog/` that accepts the extra piece, `/blog/does-not-exist` does match. The page runs, the lookup for that slug comes back empty, and the page declares it missing by calling `notFound()`. Now Next is inside the blog subtree, finds `app/blog/not-found.tsx` as the closest not-found file above the throwing component, and renders it. ```tsx // inside the blog post page const post = await getPost(slug) if (!post) notFound() ``` ## Nesting and status codes Not-found files nest the same way other conventions do: Next uses the closest one above the point where the not-found condition was raised. That lets a documentation area render a docs-flavoured "page not found" while an account area renders something completely different, with the root file as the fallback whenever no closer file exists. The rendered response carries a **404 status**, which matters more than the visuals. Crawlers must not index a missing product as if it were a real page, and monitoring should not count these as successful responses. Rendering your own "not found" markup from a normal page with a 200 status is a common mistake precisely because it looks identical in the browser and is wrong everywhere else. A `not-found.tsx` file takes no props — there is no error object and no reset callback, because nothing failed. Keep it to static content plus navigation: a heading, an explanation, and links back into the app. ## Not-found versus error These two conventions get conflated constantly, and interviewers probe it because it reveals whether someone has actually shipped an App Router app: | | not-found | error | |---|---|---| | Meaning | valid request, resource does not exist | something broke while rendering | | Status | 404 | 500-class | | Props | none | the error and a reset callback | | Environment | can be a server component | must be a client component | | Triggered by | routing failure, or `notFound()` in the subtree | a thrown render error | A missing database row is a not-found. A database that refused the connection is an error. Sending the first through the error convention tells crawlers and dashboards your application is broken when it is behaving correctly; sending the second through not-found hides a real outage. ## How to answer this in an interview Lead with the distinction: unmatched URL versus matched-route-missing-resource. Then say the consequence — the root file owns unmatched URLs because there is no segment context, nested files own the resource-missing case because there is one. Finish with the fix for the example: a segment that accepts the path plus an explicit not-found call in the page. That sequence shows you understand routing order rather than having memorised filenames.

  • Once /blog/[slug] exists, what makes app/blog/not-found.tsx render instead of the root one?
    The route now matches, so Next is rendering inside the blog subtree when the page calls `notFound()`. It then looks upward from that point for the closest not-found file and finds `app/blog/not-found.tsx`. The root file is only used when nothing closer exists, or when the URL matched no route at all.
  • What status code does a rendered not-found file return, and why does it matter?
    404. It matters because search engines must not index a missing resource as a real page, and monitoring must not count it as a successful response. Hand-rolling a "not found" screen from an ordinary page returns 200 — visually identical, wrong for every consumer that reads the status.
  • Does not-found.tsx receive props the way error.tsx does?
    No. Nothing failed, so there is no error object and no reset callback to hand it. It renders static content — a message and links back into the app. That difference is a useful tell for whether someone has confused the two conventions.

saying these in an interview costs you the question

  • Thinks a nested not-found file makes unmatched URLs match that segment
  • Says any 404 anywhere renders the root file only
  • Uses the error convention for a missing database record
  • Renders a hand-rolled not-found screen with a 200 status
  • Expects not-found.tsx to receive an error prop

context