Next.js

Return a real 404 instead of a not found message with status 200

Kamruz
Kamruz
Oct 14, 2026
5 min read
Return a real 404 instead of a not found message with status 200

A deleted article URL displays a friendly message saying the article could not be found. Monitoring considers the request successful because the response status is 200. A broken internal link remains unnoticed, and a crawler may treat the page as a soft missing result. The visible message is useful, but it does not establish the HTTP meaning of the response.

This example assumes Next.js 15 App Router, specifically behavior relevant to 15.1.11. notFound() selects the route's missing-resource UI and injects noindex metadata. There is an important streaming caveat: the not-found convention can return 200 for a streamed response and 404 for a non-streamed response. A blanket promise that calling the function always produces an HTTP 404 is therefore inaccurate.

Reproduce with an HTTP request

Choose three synthetic paths: an existing public article, a nonexistent slug, and an unpublished article. Request each directly and record status, response headers, body, and robots metadata. Do not infer status from the page's heading or from a client navigation screen. A browser can show missing-resource UI after the original document response has already succeeded.

Also test a database outage in an isolated environment. If the repository catches every error and returns null, the route can claim an article is missing when it actually cannot read storage. That harms both diagnosis and recovery. A missing row, a denied public read, and a failed dependency should have explicit meanings in the data contract.

Check whether a loading boundary starts streaming before the lookup finishes. Once response headers have been sent, the application cannot simply replace the original status. The timing of the lookup matters when a strict HTTP missing-resource status is part of the product requirement.

Give the repository a precise absence result

Use a published-only lookup that returns null when no public resource exists and throws or returns a typed operational error when storage fails. Do not catch an error solely to keep the component rendering. A friendly service-error screen can still be supplied through the application's error boundary.

Validate slugs before querying. A malformed identifier can be rejected or treated as absent according to the route contract, but it should not trigger a database cast exception that becomes an accidental 500. Keep this validation narrow: a syntactically valid slug can still refer to no record.

When drafts are private, anonymous requests should not learn that a particular unpublished resource exists unless the product explicitly allows that distinction. A privileged preview can have a separate access path. Avoid using an administrator's broad repository read for a public route and then forgetting to check publication status in metadata generation.

Invoke the framework mechanism deliberately

A page that returns a paragraph saying not found is still a normal successful render. Use notFound() after the public lookup establishes absence, and place the intended UI in the segment's not-found.tsx file. Keep the lookup before expensive downstream rendering when possible.

import { notFound } from 'next/navigation'
import { readPublicArticle } from '@/lib/public-articles'

export default async function Page({ params }: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const article = await readPublicArticle(slug)
if (article === null) notFound()
return <article><h1>{article.title}</h1></article>
}

The helper in this example must preserve operational failures. Do not wrap the page in a catch-all that swallows the framework's thrown control-flow signal and returns a generic paragraph. If application logging catches errors, let framework routing signals propagate through the documented mechanism.

For an API endpoint, return an explicit 404 response when appropriate; route UI conventions are not a substitute for the API's transport contract. External clients need a machine-readable error shape as well as the status.

Decide how strict status interacts with streaming

If an article lookup occurs before any response is streamed, the framework can return the intended missing status. If a parent or loading boundary commits the stream first, the later missing result may keep status 200 while rendering missing UI and noindex. Inspect the actual route structure and request timing rather than assuming a particular result from a file name.

For a site requiring consistent direct-request 404 responses, ensure the existence decision is made before streaming begins for the affected route. Verify this in the built application and hosting environment. Do not claim that an incidental local result proves all streamed requests will have identical status.

Streaming still has value for real resources with slow secondary sections. The tradeoff is where you place the existence check relative to those sections. Resolve the primary resource first, then stream optional details when the framework and route design support it. This keeps the missing-resource decision independent from slow recommendations or analytics.

Verify direct requests and navigation separately

A regression check should request the nonexistent slug directly and assert the expected transport status for the chosen non-streamed path. It should also verify the missing UI and noindex metadata. Run a second browser check for navigation from a valid article to a missing one, because that journey uses a different request pattern.

Test an article that is withdrawn after being cached. Invalidate its public route and discovery surfaces so an earlier successful result does not continue serving indefinitely. Check both old and new slugs after a rename. A redirect for a renamed resource can be more useful than a missing page, but it must point to the correct canonical destination.

Add an isolated dependency-failure test that expects an operational error path. This prevents a future assistant-generated catch block from turning storage failures into missing records. See invalid input and 500 responses for related error classification.

Review the user-facing recovery

A missing page should offer a sensible next action, such as the article index or search. Preserve a useful document title and avoid displaying raw database errors. If a request identifier is shown for support, it should be an opaque correlation value rather than internal infrastructure details.

Ask an AI reviewer to trace every return and catch around the lookup, then identify when headers may already have been committed. Require the review to acknowledge the streaming caveat. An answer that simply says to call notFound() has identified the mechanism but has not verified the status requirement.

Do not redirect every missing article to the homepage. That can confuse readers, hide broken links, and obscure the resource's absence. Use redirects for known moves, and missing responses for resources that truly have no public replacement.

Keep discovery consistent

A real missing route should disappear from public listings and the sitemap. Otherwise the application continues advertising URLs it cannot serve. Sitemap publication checks cover that consistency check, while content freshness covers stale successful pages after withdrawal.

The smallest safe repair joins three facts: the repository knows whether a public record exists, the route invokes the correct missing mechanism, and verification observes the actual HTTP response. Treat any one of those facts alone as incomplete evidence.

Sources

About the Author

Kamruz

Kamruz

Full Stack Developer

A passionate developer who loves creating modern web applications and sharing knowledge with the community.

More About Me

Related Articles