
Common Next.js Errors and How to Fix Them
Most Next.js errors aren't really bugs in your logic. They're the framework telling you that code ended up on the wrong side of the server-client boundary, that a request-time value was read somewhere it can't be known ahead of time, or that a convention changed between versions. Once you recognize the pattern behind an error, the fix is usually a one-line change.
This post is a field guide to the errors you're most likely to see in a Next.js 16 App Router project. For each one you'll get the message (or close to it), what's actually going on, and how to fix it. They're grouped by area: the server-client boundary, request-time data and prerendering, routing and navigation, assets and configuration, and errors that show up after upgrading to Next.js 16.
Before diving in, one general tip: read the whole error, including the "Learn more" link. Many Next.js errors link to a page under nextjs.org/docs/messages that lists every fix with its trade-offs, and in development the stack trace points at your source file and line.
Server and Client Component Errors
"This React Hook only works in a Client Component"
You're importing a component that needs `useState`. This React Hook only works
in a Client Component. To fix, mark the file (or its parent) with the
`"use client"` directive.
Cause: Components in the app directory are Server Components by default. Hooks like useState, useEffect, useContext, and useRouter only run in the browser, so a Server Component can't use them. The same error appears for other client-only APIs such as createContext.
Fix: Add "use client" at the very top of the file that uses the hook, above all imports:
// components/toggle.tsx
"use client";
import { useState } from "react";
export function Toggle() {
const [on, setOn] = useState(false);
return <button onClick={() => setOn(!on)}>{on ? "On" : "Off"}</button>;
}
Keep the directive as low in the tree as possible. Marking a whole page as a Client Component to use one hook sends that page's code to the browser and loses server-side data fetching. Extract the interactive part into its own small component instead. The "use client" directive explained goes deeper on where to draw that line.
"Event handlers cannot be passed to Client Component props"
Error: Event handlers cannot be passed to Client Component props.
<button onClick={function onClick} children=...>
^^^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
A close relative reads "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"."
Cause: Props passed from a Server Component to a Client Component must be serializable, because they travel over the network in the RSC payload. Functions can't be serialized. Writing onClick in a Server Component, or passing a callback prop like onSelect={handleSelect} into a Client Component, triggers this.
Fix: Move the handler into a Client Component. If the function needs to run on the server (to write to a database, for example), make it a Server Action with "use server". Server Actions can be passed as props because Next.js sends a reference to them, not the code:
// app/actions.ts
"use server";
export async function archiveItem(id: string) {
// runs on the server
}
// app/items/archive-button.tsx
"use client";
import { archiveItem } from "@/app/actions";
export function ArchiveButton({ id }: { id: string }) {
return <button onClick={() => archiveItem(id)}>Archive</button>;
}
The same serialization rule applies to class instances and objects with methods. Convert them to plain data before passing them across. For what's safe to pass, see passing data from Server Components to Client Components.
"async/await is not yet supported in Client Components"
Cause: You've written export default async function in a file marked "use client". Only Server Components can be async.
Fix: Either remove "use client" and keep the component on the server (where it can await data directly), or keep it a Client Component and fetch data a different way: receive it as a prop from a Server Component, unwrap a promise prop with React's use(), or use a client data library like SWR or TanStack Query.
"Module not found: Can't resolve 'fs'"
Module not found: Can't resolve 'fs'
You'll also see variations with net, tls, child_process, or path.
Cause: A Node.js-only module ended up in the client bundle. Usually a Client Component imports a file that (directly or through other imports) imports a database driver, a file system helper, or an SDK meant for the server. Everything a "use client" file imports becomes part of the browser bundle.
Fix: Trace the import chain from the Client Component and break it. Common approaches:
- Move the server code into a Server Component or Server Action and pass only the result to the client.
- Split mixed utility files into server and client versions, so client code never imports server modules.
- Add
import "server-only"at the top of server modules. Then if a Client Component ever imports one, you get a clear build error pointing at the problem instead of a confusing missing-module error.
// lib/db.ts
import "server-only";
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();
The server-only package guide covers this pattern in detail.
"Element type is invalid"
Cause: Often a compound component pattern crossing the boundary. If a Client Component exposes subcomponents as static properties (Menu.Item), a Server Component that imports it receives a client reference, not the real function, so Menu.Item is undefined. It can also be a plain import mistake: a default import of a module that only has named exports, or vice versa.
Fix: Check the import style first. For compound components used from Server Components, export each piece as a named export (MenuItem) instead of a static property.
"Hydration failed because the server rendered HTML didn't match the client"
Cause: A Client Component rendered different output in the browser than on the server: a locale-dependent date, a typeof window check, Math.random(), invalid HTML nesting, or a browser extension modifying the page.
Fix: Make the first client render match the server, then update with browser-only values after hydration. This error has enough causes that it gets its own guide: Fixing Hydration Mismatch Errors in Next.js.
Request-Time Data and Prerendering Errors
params or searchParams Is a Promise
Symptoms include undefined values, TypeScript errors like Property 'slug' does not exist on type 'Promise<...>', or a build-time type error that your page props don't satisfy the expected constraint.
Cause: In Next.js 15, params, searchParams, cookies(), headers(), and draftMode() became async, with a temporary synchronous fallback. Next.js 16 removed the fallback entirely. Code written for Next.js 14 that reads params.slug directly no longer works.
Fix: Await them:
// app/blog/[slug]/page.tsx
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}
The same applies in layouts, Route Handlers, generateMetadata, and the metadata image files. For typing, Next.js generates global PageProps and LayoutProps helpers (for example PageProps<'/blog/[slug]'>) that get these shapes right automatically. In Client Components, unwrap a params promise with use(params), or use the useParams() hook.
For large codebases, the official codemod handles most of the migration:
npx @next/codemod@canary upgrade latest
"Missing Suspense boundary with useSearchParams"
useSearchParams() should be wrapped in a suspense boundary at page "/products".
Cause: A Client Component calls useSearchParams() on a route that's being prerendered. The query string isn't known at build time, so Next.js needs a Suspense boundary to know which part to render on the client. Without one, the build fails. It doesn't happen in next dev, which is why it often surprises people in CI.
Fix: Wrap the component that calls useSearchParams in Suspense:
// app/products/page.tsx
import { Suspense } from "react";
import { Filters } from "./filters";
export default function ProductsPage() {
return (
<>
<Suspense fallback={<div className="filters-skeleton" />}>
<Filters />
</Suspense>
{/* the rest of the page is still prerendered */}
</>
);
}
Alternatively, if the page is a Server Component, read the searchParams prop there and pass values down to the Client Component as props.
"Next.js encountered uncached or runtime data during prerendering"
With cacheComponents enabled, you may see this during next build or in the dev overlay:
Error: Route "/products/[id]": Next.js encountered uncached or runtime data during prerendering.
`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()`
accessed outside of `<Suspense>` prevents the route from being prerendered.
Ways to fix this:
- [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
- [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"`
- [block] Set `export const instant = false` to allow a blocking route
Cause: Cache Components prerenders a static shell for every route. Request-time data (cookies, headers, params, search params) and uncached data (fetches, database queries) can't be part of that shell. If they're read outside a Suspense boundary, nothing can be prerendered, so Next.js stops you instead of silently making the whole page slow.
Fix: Pick one of the options the error lists:
- Stream it: wrap the component that reads the data in
Suspense(or add aloading.tsxfor the segment). The fallback becomes part of the static shell. - Cache it: if the data isn't user-specific, mark the function or component with
"use cache"so it can be included in the prerender. - Block: accept a blocking route for this page, if you deliberately don't want a shell.
// app/products/[id]/page.tsx
import { Suspense } from "react";
import { ProductDetails } from "./product-details";
export default function Page({ params }: { params: Promise<{ id: string }> }) {
return (
<Suspense fallback={<p>Loading product...</p>}>
<ProductDetails params={params} />
</Suspense>
);
}
Here ProductDetails awaits params and fetches data inside the boundary. If the error's stack trace isn't clear enough in a production build, run next build --debug-prerender for unminified output and source maps. Related variants of this error cover Math.random(), Date.now(), and new Date() in prerendered code; those also need to be cached, moved behind a boundary, or moved to the client. The "use cache" directive guide explains the caching side.
Routing and Navigation Errors
redirect() Doesn't Work Inside try/catch
Symptom: You call redirect("/dashboard") in a Server Action, but instead of redirecting, your catch block runs and logs an error with the message NEXT_REDIRECT.
Cause: redirect() and notFound() work by throwing a special error that Next.js catches higher up. A surrounding try/catch intercepts it first.
Fix: Call redirect outside the try block:
// app/actions.ts
"use server";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
export async function createProject(formData: FormData) {
let projectId: string;
try {
const project = await db.project.create({
data: { name: String(formData.get("name")) },
});
projectId = project.id;
} catch (error) {
console.error(error);
return { error: "Could not create the project" };
}
redirect(`/projects/${projectId}`);
}
If you must call it inside a try, rethrow it: the unstable_rethrow function from next/navigation rethrows Next.js internal errors and lets you handle everything else.
"invariant expected app router to be mounted"
Cause: A component using useRouter, usePathname, or useSearchParams from next/navigation rendered outside the App Router. Most often this happens in unit tests, Storybook, or when a component built for the App Router is used from the pages directory.
Fix: In tests and Storybook, mock next/navigation. In the pages directory, use the Pages Router's useRouter from next/router instead.
Parallel Route Slot Missing default.js
Cause: Next.js 16 requires every parallel route slot (@modal, @sidebar, and so on) to have a default.tsx file. Without one, the build fails.
Fix: Add a default.tsx that returns null (or calls notFound()):
// app/@modal/default.tsx
export default function Default() {
return null;
}
"Failed to find Server Action"
Cause: A browser tab loaded with a previous deployment tries to call a Server Action, but the action ID changed in the new deployment. It also happens when instances behind a load balancer run different builds or use different encryption keys.
Fix: Prefer rolling deployments, keep NEXT_SERVER_ACTIONS_ENCRYPTION_KEY the same across all instances of a deployment, and handle the failure in the UI with a "please refresh" message rather than a hard crash.
Assets and Configuration Errors
next/image: Hostname Is Not Configured
Error: Invalid src prop (https://images.example.com/photo.jpg) on `next/image`,
hostname "images.example.com" is not configured under images in your `next.config.js`
Cause: next/image only optimizes external images from hosts you've explicitly allowed, to prevent your server from being used as an open image proxy.
Fix: Add the host to images.remotePatterns. The older images.domains option is deprecated in Next.js 16.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.example.com",
pathname: "/uploads/**",
},
],
},
};
export default nextConfig;
Keep the pattern as narrow as you can. Two related Next.js 16 changes: local images with query strings now need images.localPatterns, and images from private IP addresses are blocked unless you set images.dangerouslyAllowLocalIP. The next/image guide covers configuration in detail.
Environment Variable Is undefined in the Browser
Cause: Only variables prefixed with NEXT_PUBLIC_ are exposed to client code, and they're inlined at build time. A variable named API_URL is undefined in a Client Component, and changing a NEXT_PUBLIC_ value after the build has no effect on the client bundle.
Fix: Rename variables that are safe to expose to NEXT_PUBLIC_* and rebuild. Never add the prefix to secrets. For values that must change per environment without a rebuild, read them on the server (where process.env is read at runtime in dynamic code) and pass them down as props. Next.js 16 also removed serverRuntimeConfig and publicRuntimeConfig, so environment variables are the way to go.
"Error: Cannot find module" After Switching Branches
Cause: A stale build cache in .next that references files that no longer exist.
Fix: Stop the dev server, delete the .next folder, and start again. If dependency changes are involved, reinstall with npm ci as well.
Errors After Upgrading to Next.js 16
Build Fails Because a webpack Config Was Found
Cause: Turbopack is the default bundler for both next dev and next build in Next.js 16. If your next.config has a webpack function (often added by a plugin you installed, not by you), next build fails rather than silently ignoring it.
Fix: You have three options:
- Migrate the customization to Turbopack's options (
turbopackinnext.config), then remove thewebpackfunction. - Build with Turbopack anyway and ignore the webpack config:
next build --turbopack. - Keep webpack for now:
next build --webpack.
middleware.ts Deprecation Warning
Cause: Next.js 16 renamed middleware.ts to proxy.ts, and the exported middleware function to proxy. The old name still works but is deprecated.
Fix: Rename the file and the function:
mv middleware.ts proxy.ts
// proxy.ts
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
if (!request.cookies.has("session")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};
Note that proxy runs on the Node.js runtime and can't be configured for the Edge runtime. If you depend on the Edge runtime, keep middleware.ts for now. Background on what this file does is in understanding middleware in Next.js.
"next lint" Is Not a Command
Cause: Next.js 16 removed next lint, and next build no longer runs linting. The eslint option in next.config is also gone.
Fix: Run ESLint (or Biome) directly. Update your script to "lint": "eslint ." and use flat config (eslint.config.mjs), which @next/eslint-plugin-next now defaults to. The codemod npx @next/codemod@canary next-lint-to-eslint-cli . automates the switch.
A General Approach to Unfamiliar Errors
When you hit an error that isn't on this list:
- Read the full message and follow the "Learn more" link. Next.js error pages are written to be actionable.
- Find out where the code runs. A large share of Next.js errors come from code on the wrong side of the server-client boundary.
- Reproduce in a production build. Run
next build && next start. Some errors (Suspense requirements, prerender failures) only appear there. - Clear caches. Delete
.nextwhen errors don't match your current code. - Use a debugger. Breakpoints on both the server and client side are covered in debugging Next.js in VS Code and Chrome DevTools.
- Check your versions. Run
npx next infoto see Next.js, React, and Node.js versions, and compare with the upgrade guide if you've recently updated.
Conclusion
Most Next.js errors fall into a few families: client-only code in Server Components, non-serializable values crossing into Client Components, request-time data read where the framework expects to prerender, and conventions that changed in Next.js 15 and 16. Learn to recognize those families and you'll usually know the fix before you finish reading the stack trace: add "use client" lower in the tree, move a function into a Server Action, await params, add a Suspense boundary, allow an image host, or rename a file.
Keep this page handy for the next red overlay, and when in doubt, follow the link in the error message. It usually leads straight to the fix.


