Next.js 404 Page Not Found Error: Common Causes and Fixes in Next.js 13
Next.js returns a 404 "Page Not Found" error when the resolveRoutes function fails to match a request to a physical file or rewrite, triggering the fallback handler in router-server.ts that renders the static 404 page.
When building applications with the vercel/next.js repository, encountering a persistent nextjs 404 page not found error despite having defined routes is a common debugging scenario. Understanding how the framework's routing resolution works internally allows you to trace exactly where the request fails to match your expected page file.
How Next.js 13 Handles Routing and 404 Resolution
Next.js determines the final response for every request in two distinct phases implemented in the server core:
- Route Resolution – The
resolveRoutesfunction inpackages/next/src/server/lib/router-utils/resolve-routes.tswalks through an ordered list of routes (rewrites, redirects, middleware, static-file checks, and dynamic-route matching) to decide which output should handle the request. - Render Fallback – If
resolveRoutesreturns no matching output, the server falls back to the built-in 404 handling inrouter-server.ts(packages/next/src/server/lib/router-server.ts).
The critical logic that produces the final 404 response appears at the end of handleRequest:
// packages/next/src/server/lib/router-server.ts:L66-L81
if (appNotFound) {
return await invokeRender(parsedUrl, UNDERSCORE_NOT_FOUND_ROUTE, …);
}
await invokeRender(parsedUrl, '/404', …);
If appNotFound is false (meaning no custom /404 page exists in the App Router) and the routing step never returned a matching output, Next.js invokes the static 404 page.
Common Causes of Next.js 404 Errors Despite Defined Routes
Incorrect File Location for Your Router Type
The most frequent cause of a nextjs 404 page not found error is placing page files in the wrong directory for your routing architecture. The resolveRoutes function checks fsChecker.getItem(pathname) (L71-L78 in resolve-routes.ts) to verify if a file exists at the expected path.
- Pages Router: Files must live inside
pages/(e.g.,pages/about.tsxfor/about) - App Router: Files must live inside
app/using thepage.tsxconvention (e.g.,app/about/page.tsx)
If you place a file in app/about.tsx instead of app/about/page.tsx, or place it in pages/ when using the App Router, the filesystem checker returns no match, triggering the 404 fallback.
Base Path and Asset Prefix Mismatches
When basePath is configured in next.config.js, the server strips this prefix before matching routes. In router-server.ts, the framework normalizes the URL by removing the base path.
If you configure:
// next.config.js
module.exports = {
basePath: "/docs",
};
Then your file pages/help.tsx is reachable at /docs/help, not /help. Requesting /help results in a 404 because the router strips the base path (finding none), then fails to match help.tsx against the root path.
Internationalization (i18n) Locale Handling
For applications using the i18n configuration, resolveRoutes normalizes locale paths (L97-L135 in resolve-routes.ts). If the request is /fr/about but the page exists only under the default locale without proper locale-specific routing, the matcher may fail to resolve the file.
Ensure that:
- Locale-specific pages exist in
pages/[locale]/or use the App Router's locale handling - The
i18nconfiguration innext.config.jscorrectly maps locales to domains or paths
Middleware Rewrites and Redirects
Middleware that rewrites requests to non-existent paths causes 404 errors after the rewrite occurs. In resolve-routes.ts, middleware handling processes rewrites before filesystem checks.
If your middleware.ts contains:
import { NextResponse } from 'next/server';
export function middleware(req) {
return NextResponse.rewrite(new URL('/does-not-exist', req.url));
}
Every request rewrites to /does-not-exist, which likely has no corresponding page file. The router then falls through to the 404 handler in router-server.ts.
Conflicting Public Files
A file in the public/ directory with the same path as a page file causes the router to abort with an error. According to router-server.ts, when matchedOutput.itemPath collides with a public asset, the framework prevents ambiguous resolution.
If you have both:
The router detects the conflict and fails to serve either, often resulting in a 404 or build-time error. Rename the public asset to public/about-static.html to resolve the conflict.
Trailing Slash Configuration
The trailingSlash: true configuration in next.config.js requires URLs to end with a slash. The maybeAddTrailingSlash logic in resolve-routes.ts handles this normalization.
If trailing slash is enabled but the request omits the slash (e.g., /about instead of /about/), Next.js returns a 308 redirect to the slashed version. If the slashed version also fails to match (due to file naming issues), the user sees a 404.
Debugging the 404 Flow in Source Code
To trace why your specific route returns nextjs 404 page not found, examine these key locations in the vercel/next.js source:
-
Route Resolution Entry:
packages/next/src/server/lib/router-utils/resolve-routes.ts- The
resolveRoutesfunction iterates through rewrites, redirects, headers, middleware, and filesystem checks - Line 71-78:
fsChecker.getItem(pathname)verifies physical file existence
- The
-
Request Handler:
packages/next/src/server/lib/router-server.ts- The
handleRequestfunction orchestrates the response - Line 21-23: Base path stripping logic
- Line 66-81: Final 404 fallback logic using
invokeRender(parsedUrl, '/404', …) - Line 496: Public file conflict detection
- The
-
Path Utilities:
packages/next/src/shared/lib/router/utils/path-has-prefix.ts- Helper for base path and asset prefix normalization
-
i18n Handling:
packages/next/src/shared/lib/i18n/normalize-locale-path.ts- Locale extraction and path normalization for internationalized routes
When debugging, add logging or breakpoints at resolve-routes.ts to inspect the pathname after normalization and verify whether fsChecker.getItem returns a valid item or undefined.
Practical Solutions and Code Examples
Verifying Correct Page Placement for Pages Router
For projects using the Pages Router, ensure your file structure matches the URL path exactly:
// File: pages/blog/[slug].tsx
// URL: /blog/hello-world
import { useRouter } from 'next/router';
export default function BlogPost() {
const router = useRouter();
return <h1>Post: {router.query.slug}</h1>;
}
If you place this file in app/blog/[slug]/page.tsx while the project uses the Pages Router, the route will return nextjs 404 page not found because resolveRoutes searches pages/ for the Pages Router.
Configuring Base Path Correctly
When using basePath, structure your files relative to the root, but access them via the prefixed URL:
// next.config.js
module.exports = {
basePath: '/documentation',
trailingSlash: true,
};
// File: pages/getting-started.tsx
// Accessible at: /documentation/getting-started/
export default function GettingStarted() {
return <div>Setup Guide</div>;
}
Requesting /getting-started without the /documentation prefix results in a 404 because router-server.ts strips the basePath (finding none), then fails to match the file.
Resolving Middleware Rewrite Conflicts
Inspect your middleware to ensure rewrites point to valid destinations:
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
// Check if the rewrite target exists before rewriting
if (request.nextUrl.pathname.startsWith('/legacy')) {
return NextResponse.rewrite(new URL('/legacy-portal', request.url));
}
return NextResponse.next();
}
If /legacy-portal has no corresponding pages/legacy-portal.tsx or app/legacy-portal/page.tsx, the rewrite will trigger the 404 handler in router-server.ts.
Summary
- Next.js 404 errors occur when
resolveRoutesinpackages/next/src/server/lib/router-utils/resolve-routes.tsfails to match a request to a filesystem item, middleware rewrite, or dynamic route, forcing the fallback torouter-server.ts. - File location is the most common culprit: Pages Router requires files in
pages/, while App Router requiresapp/**/page.tsx. - Configuration mismatches in
basePath,trailingSlash, ori18nsettings normalize URLs before matching, often causing unexpected 404s if file paths don't account for these prefixes. - Middleware rewrites and public file conflicts can divert requests to non-existent paths or trigger resolution aborts before reaching your page component.
- Debugging requires inspecting the
pathnameafter normalization inresolve-routes.tsand verifyingfsChecker.getItemreturns a valid output before the request reaches the 404 fallback inrouter-server.ts.
Frequently Asked Questions
Why does my Next.js 13 app return 404 even though the file exists in the pages directory?
This typically happens when your project is configured to use the App Router (the default in Next.js 13+) but you placed the file in pages/, or vice versa. The resolveRoutes function in packages/next/src/server/lib/router-utils/resolve-routes.ts checks fsChecker.getItem(pathname) against the active router's directory. If you have app/page.tsx but are looking for /about at pages/about.tsx, the filesystem checker returns undefined, triggering the 404 fallback in router-server.ts.
How does the basePath configuration cause 404 errors in Next.js?
When basePath is set in next.config.js, the server strips this prefix from incoming URLs before attempting to match routes, as implemented in packages/next/src/server/lib/router-server.ts (lines 21-23). If you configure basePath: "/docs" but place your file at pages/index.tsx, the route is only accessible at /docs/, not /. Requesting the root path results in a 404 because the router strips the basePath (finding none), then fails to locate a file matching the remaining empty path.
Can Next.js middleware cause 404 errors even when the page file exists?
Yes. Middleware runs before the filesystem check in resolve-routes.ts (lines 87-165). If your middleware.ts rewrites requests to a path that lacks a corresponding page file, the router continues through the resolution phases, finds no matching output, and falls back to the 404 handler. For example, rewriting all requests to /maintenance when pages/maintenance.tsx does not exist will result in a 404 for every route, regardless of whether the original target pages exist.
What is the difference between the App Router and Pages Router 404 handling?
In the Pages Router, 404 handling relies on pages/404.tsx or the default static 404 page, with resolution occurring through fsChecker.getItem looking for files in the pages/ directory. In the App Router, 404 handling uses the not-found.tsx file convention, and the router checks for app/not-found.tsx or app/[...]/not-found.tsx when appNotFound is true in router-server.ts (lines 66-81). The App Router also supports parallel routes and intercepting routes, which add additional resolution layers not present in the Pages Router's simpler filesystem-based routing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →