/ โ† HTTP Error Fixes / Next.js Error Pages
In this guide
  1. How Next.js error handling works
  2. Common causes of broken error pages in Next.js
  3. How to diagnose the problem
  4. The key files and code
  5. Fixing MIDDLEWARE_INVOCATION_FAILED on Vercel
  6. A worked example
  7. The fix, step by step
  8. Edge cases worth knowing
  9. Keeping your error pages correct
  10. Questions and answers

How Next.js error handling works

Next.js gives you a separate file for each kind of failure, and each one has a different job. If you mix them up, visitors see a blank screen, or Google sees a page that says it is missing but answers with a 200 status code. Getting the files right is mostly about knowing which failure each one catches.

In the App Router (the app folder), these are the files that matter:

FileWhat it catchesStatus code it should produce
not-found.jsURLs that match no route, and any call to notFound()404
error.jsErrors thrown while rendering a route segment and its children500 for a server render error
global-error.jsErrors in the root layout or root template, where error.js cannot help500
middleware.js or proxy.jsNothing: it runs before routing, and a crash here fails the whole request500 (MIDDLEWARE_INVOCATION_FAILED on Vercel)

The Pages Router (the pages folder) uses older equivalents: pages/404.js for missing pages, pages/500.js for server errors, and pages/_error.js as a catch-all that receives the status code. Both routers can live in the same project, and the App Router files take over for routes inside app.

Two rules explain most problems. First, error.js is a React error boundary, so it wraps the segment's page and children, but not the layout.js in the same folder. An error in a layout bubbles up to the parent segment's error.js, and an error in the root layout goes to global-error.js. Second, the status code is decided by the server when the response starts. Once Next.js has begun streaming HTML to the browser, it cannot change the status, so a notFound() call that happens late in a streamed page is sent with a 200 and a noindex robots tag instead of a 404.

๐Ÿ’ก Google treats a page that says not found but returns 200 as a soft 404. It will usually drop it from the index, but the URL keeps being crawled and wastes crawl budget. A real 404 status is always cleaner.

Common causes of broken error pages in Next.js

Most Next.js error page problems come from a short list of mistakes. Check your project against this table before you change any code.

SymptomLikely causeWhere to look
Missing product shows an empty page with a 200Data fetch returns null and the page renders anyway instead of calling notFound()The dynamic route, for example app/products/[slug]/page.js
Default black and white 404 instead of your designNo app/not-found.js, or it sits in a segment that unmatched URLs never reachRoot of the app folder
White screen with Application error: a client-side exceptionAn error in the browser with no error.js above itBrowser console, then the nearest error.js
Root layout crash shows the default errorNo global-error.js, or it does not render its own html and body tagsapp/global-error.js
500 INTERNAL_SERVER_ERROR, code MIDDLEWARE_INVOCATION_FAILEDMiddleware throws, uses an unsupported API, or reads a missing environment variablemiddleware.js or proxy.js and Vercel logs
Every page 404s after deploybasePath, trailingSlash or a rewrite in next.config.js no longer matches the URLsnext.config.js
Error page itself returns 500The error component imports something that also fails, such as the same broken data callerror.js imports

The first row is the one that hurts search the most. A catch-all or dynamic route that shows a friendly message for an unknown slug, without calling notFound(), turns every mistyped or deleted URL into an indexable thin page. Our 404 errors guide explains why a real 404 is the right answer for content that has gone.

How to diagnose the problem

Start by finding out what status code each failing URL really returns. The browser page can look correct while the status is wrong, so test with curl rather than by eye:

# Status line and headers only
curl -sI https://www.example.com/products/does-not-exist

# Follow redirects and print just the final status
curl -s -o /dev/null -w "%{http_code} %{url_effective}\n" -L https://www.example.com/old-page

# Look for the noindex tag Next.js adds when the status could not be changed
curl -s https://www.example.com/products/does-not-exist | grep -i 'name="robots"'

A missing page should print 404. If it prints 200 and the grep finds a noindex robots tag, the page is streaming before notFound() runs. If it prints 200 with no robots tag, the route is not calling notFound() at all.

Finding a MIDDLEWARE_INVOCATION_FAILED error

On Vercel, this 500 means the middleware function threw or could not run. The error page shows only the code and an ID, so you need the logs. Open the project in the Vercel dashboard, go to Logs, and filter by the failing path. The log entry for the middleware invocation holds the real exception, such as a TypeError or a missing module. From the command line you can stream logs for a deployment:

npx vercel logs https://my-app-abc123.vercel.app

Reproduce locally with next build followed by next start, not just next dev. The development server is more forgiving, and some middleware errors only appear in a production build.

Checking for client-side errors

If the page renders and then turns white, open Chrome DevTools, go to the Console, and reload. The first red error is usually the cause. A missing error.js above that component is why the whole page disappears instead of showing a recovery message. For broader checks across a site, the Next.js site audit guide covers crawling a build for status codes.

The key files and code

A not-found page that returns a real 404

Put not-found.js at the root of app so it handles every unmatched URL. Keep it a Server Component and avoid slow data calls in it.

// app/not-found.js
import Link from 'next/link';

export const metadata = { title: 'Page not found | Example Bakery' };

export default function NotFound() {
  return (
    <main>
      <h1>We could not find that page</h1>
      <p>The page may have moved or been removed.</p>
      <Link href="/">Back to the home page</Link>
    </main>
  );
}

Then make every dynamic route call notFound() as soon as it knows the item does not exist, before anything streams:

// app/products/[slug]/page.js
import { notFound } from 'next/navigation';
import { getProduct } from '@/lib/products';

export default async function ProductPage({ params }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  if (!product) notFound();
  return <h1>{product.name}</h1>;
}

Call the same check in generateMetadata if the route has one. Metadata is resolved before the page streams, so a notFound() there reliably produces a 404. Avoid wrapping the whole page in a loading.js or a Suspense boundary that starts streaming before the lookup finishes.

An error boundary for a segment

// app/products/error.js
'use client';

import { useEffect } from 'react';

export default function Error({ error, reset }) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  return (
    <div>
      <h2>Something went wrong loading this page</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

The 'use client' line is required, because error boundaries run in the browser. In production, server errors reach the component with a generic message and a digest value so that secrets are not leaked; search your server logs for that digest to find the real stack trace.

A global error page

// app/global-error.js
'use client';

export default function GlobalError({ error, reset }) {
  return (
    <html lang="en">
      <body>
        <h1>Sorry, the site had a problem</h1>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  );
}

Because it replaces the root layout when it is active, global-error.js must render its own <html> and <body> tags. It does not get your layout's fonts or stylesheets, so keep it simple.

Middleware that cannot crash the request

// middleware.js (in Next.js 16: proxy.js, exporting a function named proxy)
import { NextResponse } from 'next/server';

export function middleware(request) {
  try {
    const country = request.headers.get('x-vercel-ip-country') || 'GB';
    const res = NextResponse.next();
    res.headers.set('x-country', country);
    return res;
  } catch (err) {
    console.error('middleware failed', err);
    return NextResponse.next();
  }
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)'],
};

The matcher stops middleware running on static files, which cuts the number of invocations and the chance of a failure on assets. The try and catch turns an unexpected error into a logged warning and a normal response rather than a 500 for every visitor.

Fixing MIDDLEWARE_INVOCATION_FAILED on Vercel

The full message usually reads 500: INTERNAL_SERVER_ERROR with Code: MIDDLEWARE_INVOCATION_FAILED and an ID. It means Vercel started your middleware and it ended with an unhandled error. Every request the matcher covers fails, which is why it often takes down a whole site at once. These are the usual reasons, roughly in order of how often they come up:

โš ๏ธ Do not fix this by deleting the matcher or the middleware without checking what it does. If it protects admin routes or sets security headers, removing it can expose pages. Make it fail safely instead.

Once the cause is fixed, redeploy and test a few paths with curl -sI. Our 500 errors guide covers the general server-side checks, and Server error (5xx) in Search Console explains what Google does if the outage lasted long enough to be crawled.

A worked example

Example Bakery runs a Next.js 15 shop on Vercel. Two things go wrong after a redesign: deleted cake pages still show in Google, and on Monday morning the whole site shows 500: INTERNAL_SERVER_ERROR, Code: MIDDLEWARE_INVOCATION_FAILED.

For the cakes, curl -sI https://www.example.com/cakes/lemon-drizzle returns HTTP/2 200. The page template shows Sorry, this cake is no longer available when the database returns nothing, but never calls notFound(). Google has kept 140 of these URLs as thin pages. The fix is two lines: import notFound and call it when the lookup is empty, in both the page and generateMetadata. After deploying, curl returns HTTP/2 404 and the URL Inspection live test in Search Console reports the 404.

For the outage, the Vercel logs for the middleware show TypeError: Cannot read properties of undefined (reading 'split'). The middleware reads a BASIC_AUTH variable to protect a staging path, and that variable was added only to the Preview environment. On Production it is undefined, so .split(':') throws on every request. The developer adds the variable to Production, changes the code to skip the check when the value is missing, narrows the matcher to /staging/:path*, and redeploys. The site recovers within a minute, and the next deploy can no longer take it down for the same reason.

The fix, step by step

Step 1

Test the real status codes

Run curl -sI against a URL you know is missing, a URL whose item was deleted, and a page that is failing. Write down the status each one returns and whether the HTML contains a noindex robots tag. This tells you whether the problem is a missing notFound() call, a streamed response, a crashing component or the middleware. Repeat the test after each change in the following steps, so you can see exactly which change fixed which symptom.

Step 2

Add a root not-found page

Create app/not-found.js at the root of the app folder with a clear message, a link back to the home page and a short metadata title. Keep it as a Server Component with no slow data calls. Build and start the app with next build and next start, then request a random path such as /zz-test-404 with curl. You should see your design in the browser and a 404 status in the response headers.

Step 3

Call notFound in every dynamic route

Open each dynamic route, such as app/products/[slug]/page.js, and call notFound() straight after the data lookup when it returns nothing. Add the same check to generateMetadata where the route has one, because metadata resolves before streaming starts. For the Pages Router, return { notFound: true } from getStaticProps or getServerSideProps. Confirm with curl that deleted items now return 404 instead of 200, and check a live item still returns 200.

Step 4

Add error boundaries

Create an error.js file with 'use client' at the top in each major segment, and one at the root of app. Give it a message and a button that calls reset(). Remember that it does not catch errors in the layout of the same folder, so place it one level up if a layout can fail. Throw a test error in a page during development and check that the boundary shows instead of a white screen.

Step 5

Add global-error.js

Create app/global-error.js as a Client Component that renders its own html and body tags, a short apology and a retry button. This is the only file that can catch an error in the root layout. Keep it free of imports from your layout, fonts or data layer, so it cannot fail for the same reason. Test it by temporarily throwing inside the root layout in a production build, then remove the test error.

Step 6

Read the middleware logs

If you see MIDDLEWARE_INVOCATION_FAILED, open the Vercel dashboard, go to Logs and filter by the failing path to find the real exception. Note the error message and line. Common findings are a property read on undefined, a missing environment variable or a Node.js-only package imported into Edge middleware. Reproduce it locally with a production build so you can test the fix before you redeploy to Production.

Step 7

Make the middleware fail safely

Fix the root cause first: add the missing variable to the Production environment, replace unsupported imports, or move heavy work into a route handler. Then wrap risky parsing in try and catch and return NextResponse.next() on failure where that is safe. Narrow the matcher so middleware skips static files and paths that do not need it. Redeploy and test several paths with curl to confirm normal 200 responses.

Step 8

Verify in Search Console

Use URL Inspection on a deleted URL and a normal page and run a live test on each. The deleted one should report a 404 and the normal one should be available to Google. In the Pages report, open Soft 404 and Server error (5xx) and use Validate fix once your changes are live. Validation can take several days to a few weeks, so keep testing with curl in the meantime.

Edge cases worth knowing

Keeping your error pages correct

Error handling breaks quietly, usually during a refactor, so add checks that run without you remembering. A short script in your deploy pipeline that requests a known missing URL and fails the build if it does not get a 404 catches most regressions:

status=$(curl -s -o /dev/null -w "%{http_code}" https://preview.example.com/this-page-does-not-exist)
[ "$status" = "404" ] || { echo "Expected 404, got $status"; exit 1; }

For the wider picture of how Next.js sites get crawled and indexed, see Next.js SEO fixes.

Questions and answers

1. What does 500 INTERNAL_SERVER_ERROR code MIDDLEWARE_INVOCATION_FAILED mean?

It means your Next.js middleware started on Vercel and then failed with an unhandled error, so Vercel returned a 500 for the request. Because middleware runs before every matching route, one bug can take the whole site down. The real cause is in the Vercel logs for the failing path, and it is usually an exception such as reading a property of undefined, an environment variable that exists in Preview but not Production, or a package that the Edge runtime cannot run.

2. How do I show a custom 404 page in Next.js?

Create a file called not-found.js at the root of the app folder and export a component from it. Next.js shows it for any URL that matches no route and for any call to the notFound function. In the Pages Router the equivalent is pages/404.js. Keep the page simple and fast, include a link to the home page or search, and test with curl that the response has a 404 status rather than a 200, because the design alone does not prove the status is correct.

3. What is global-error.js in Next.js?

It is the error boundary that wraps your entire App Router application, including the root layout, so it is the last line of defence when something fails at the top level. A normal error.js cannot catch errors in the layout of its own folder, which is why the root layout needs this separate file. When it is active it replaces the root layout, so it must render its own html and body tags, and it must be a Client Component.

4. Why does my Next.js 404 page return a 200 status?

Usually because the route never calls notFound, or calls it after the page has started streaming. A page that renders a not found message from its own template keeps the normal 200 status. If the call happens inside a Suspense boundary after HTML has been sent, Next.js cannot change the status and instead adds a noindex robots tag. Move the existence check to the top of the page or into generateMetadata so it runs before streaming begins.

5. Does error.js catch every error in Next.js?

No, error.js only catches errors thrown while rendering its route segment and the segments below it. It does not catch errors in the layout.js or template.js of the same folder, errors in event handlers such as click handlers, errors in middleware, or errors in the root layout. Layout errors go to the parent segment's error file, root layout errors go to global-error.js, and event handler errors need their own try and catch blocks in your code.

6. Are Next.js error pages bad for SEO?

Error pages are not bad for SEO when they return the correct status code. A 404 for content that has gone tells Google to drop the URL, and a short 500 tells it to try again later. The damage comes from pages that look like errors but return 200, which Google reports as soft 404s, and from long runs of 5xx errors, which can slow crawling and eventually remove pages from results if they continue for days.

7. How do I fix middleware errors on Vercel without breaking my site?

Find the real exception in the Vercel logs, fix that cause, and then make the middleware fail safely. Add any missing environment variables to the Production environment and redeploy, replace packages that need Node.js APIs, and wrap parsing of cookies, headers and URLs in try and catch. Narrow the matcher so middleware only runs on paths that need it. Avoid deleting middleware outright if it protects private pages or sets security headers, and test a handful of paths with curl after every redeploy.

Check your Next.js pages return the right status codes

Our HTTP error audit requests your URLs the way a crawler does and flags soft 404s, 5xx responses and pages whose status does not match what they show.

Open HTTP Error Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All HTTP Error Fixes  ·  Next.js SEO fixes  ·  How to fix 500 errors  ·  How to fix a custom 404 page  ·  HTTP errors in Node.js  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.