How to Fix: notFound function incorrectly triggers error overlay in Next.js 15

6 min read

The notFound() helper is supposed to short-circuit rendering and show your custom 404 UI, but in Next.js 15 development mode it can incorrectly surface the error overlay instead. This usually looks like a runtime failure even though the route logic is behaving like a valid not-found boundary trigger.

If you are reproducing this from the linked repository, the key point is that the bug is primarily a development overlay issue, not necessarily a production routing failure. In other words, the framework throws an internal not-found signal, and the dev overlay may interpret that signal as an unhandled error under specific rendering conditions.

Understanding the Root Cause

In the App Router, notFound() does not behave like a normal return value. It throws a special internal exception that Next.js catches and converts into your not-found boundary or route-level 404 page. This is part of the framework design.

The problem in Next.js 15 is that, in development, the React error overlay can sometimes intercept or display that internal thrown signal before the framework fully resolves it as a not-found state. That makes a valid 404 control flow look like a fatal application error.

This is why the issue is confusing:

  • Your code may be semantically correct.
  • The route may still resolve properly in some cases.
  • The development experience is misleading because the overlay treats framework control flow like an exception.

Technically, this tends to happen when:

  • notFound() is triggered during server rendering inside an App Router page, layout, or async data-loading path.
  • The dev server is running with enhanced diagnostics that aggressively surface thrown values.
  • A local not-found.tsx boundary is missing, misplaced, or not reached cleanly due to the render tree structure.

So the real bug is not that notFound() is conceptually wrong. The bug is that the development overlay handling is incorrect for this framework-thrown not-found signal.

Step-by-Step Solution

The safest way to handle this issue is to verify that your route structure is correct, isolate the development-only behavior, and implement the not-found boundary exactly where Next.js expects it.

1. Confirm you are using notFound() in a Server Component or supported server path

Use it in an App Router page, layout, or server-side async logic. A minimal example:

import { notFound } from 'next/navigation'

export default async function Page() {
  const data = null

  if (!data) {
    notFound()
  }

  return <div>Loaded</div>
}

This is valid usage. Do not wrap notFound() as if it were a normal boolean-based return pattern.

2. Add a route-level not-found.tsx

Create a matching not-found boundary in the relevant route segment or at the app root:

export default function NotFoundPage() {
  return (
    <div>
      <h1>404</h1>
      <p>The requested resource was not found.</p>
    </div>
  )
}

Recommended structure:

app/
  not-found.tsx
  page.tsx

Or for a nested segment:

app/
  products/
    [slug]/
      page.tsx
    not-found.tsx

This ensures Next.js has an explicit 404 rendering boundary to target.

3. Avoid catching the internal not-found signal unless you immediately rethrow it

A common mistake is wrapping route logic in a broad try/catch block:

import { notFound } from 'next/navigation'

export default async function Page() {
  try {
    const data = null

    if (!data) {
      notFound()
    }

    return <div>Loaded</div>
  } catch (error) {
    return <div>Something went wrong</div>
  }
}

This can interfere with how Next.js handles the thrown control-flow signal. Instead, let notFound() propagate naturally:

import { notFound } from 'next/navigation'

export default async function Page() {
  const data = null

  if (!data) {
    notFound()
  }

  return <div>Loaded</div>
}

If you must catch errors for another operation, separate the code paths carefully so your 404 flow is not absorbed into generic error handling.

4. Test the behavior in production mode

Because this issue is frequently dev-overlay specific, verify with a production build:

yarn build
yarn start

If the page correctly renders the not-found UI in production, that confirms the problem is likely the known development behavior rather than broken route logic.

5. Upgrade to the latest patched Next.js version

This bug is tied to framework internals, so the long-term fix is usually a Next.js patch release. Update dependencies and retest:

yarn add next@latest react@latest react-dom@latest

Then restart the dev server:

yarn dev

If your project is locked to a specific minor release, review the official Next.js issue tracker and release notes for updates related to notFound, App Router, or the development overlay.

6. Use a defensive data-loading pattern

For dynamic routes, explicitly check the fetched entity before rendering:

import { notFound } from 'next/navigation'

async function getProduct(slug) {
  const res = await fetch(`https://example.com/api/products/${slug}`, {
    cache: 'no-store'
  })

  if (!res.ok) {
    return null
  }

  return res.json()
}

export default async function ProductPage({ params }) {
  const product = await getProduct(params.slug)

  if (!product) {
    notFound()
  }

  return <div>{product.name}</div>
}

This keeps the 404 decision explicit and prevents unrelated render failures from being mistaken for missing content.

7. Distinguish real runtime errors from not-found control flow

Use error.tsx for genuine failures and not-found.tsx for missing resources. Example error boundary:

'use client'

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Something failed</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

Do not use this as a substitute for 404 handling. The two mechanisms serve different purposes in the App Router lifecycle.

  1. Keep notFound() unwrapped by generic try/catch blocks.
  2. Create the correct not-found.tsx boundary.
  3. Confirm the issue only appears in development.
  4. Upgrade Next.js to the latest version containing any relevant patch.

Common Edge Cases

Calling notFound() inside a Client Component

notFound() is intended for server-driven route handling. If you try to use it from a client-only component, the behavior may be invalid or misleading. Move the missing-resource decision to the server boundary.

Generic error wrappers swallowing the not-found signal

If a helper function or page-level abstraction catches all thrown values, it can accidentally suppress the internal 404 signal. This often causes either a fallback UI or a dev overlay instead of the expected not-found page.

Missing or incorrectly placed not-found.tsx

If the file is placed in the wrong segment, Next.js may not resolve the nearest not-found boundary you expect. Check your route hierarchy carefully.

Mixing Pages Router and App Router assumptions

The old Pages Router patterns do not map directly to App Router behavior. In App Router, notFound() is the preferred control-flow API for server-rendered missing resources.

Data fetches that throw before your null check runs

If your fetch utility throws on non-200 responses, your code may never reach notFound(). In that case, normalize the fetch result and convert missing data into a controlled null path.

Confusing redirect() and notFound()

Both APIs throw internal signals, but they represent different routing outcomes. Using the wrong one can produce hard-to-debug development behavior, especially when combined with async rendering and nested layouts.

FAQ

Why does this happen only in development?

Because the development overlay is designed to expose thrown runtime values aggressively for debugging. In this bug, it can misclassify the internal notFound() control-flow exception as a regular error before Next.js finishes converting it into a 404 response.

Is my code wrong if the overlay appears?

Not necessarily. If your route renders the correct not-found UI in production and your implementation follows App Router conventions, the issue is likely framework-side development behavior rather than an application logic bug.

What is the best workaround until the framework fix lands?

The best workaround is to keep notFound() usage minimal and explicit, ensure a valid not-found.tsx exists, avoid swallowing the thrown signal in catch blocks, and verify behavior with a production build. Then track updates in the official Next.js repository.

When debugging this issue, think of notFound() as a framework-level routing signal, not a normal function return. Once that mental model is clear, the misleading overlay becomes easier to diagnose, and the fix path is usually straightforward.

Leave a Reply

Your email address will not be published. Required fields are marked *