Next.js App Router Params Promise Breaking Change (Next 15) in Production

Next.js 15 params promise breaking change is one of those upgrades that looks clean right up until it isn't — locally, everything ran and type-checked fine, and then a genuinely fresh build somewhere else throws errors treating params like the plain object it's always been, when Next.js 15 quietly turned it into a Promise instead. Nothing about your route logic broke. The shape of the data Next.js hands your component changed underneath it.
Short answer: params and searchParams in page.js, layout.js, and route.js — along with cookies(), headers(), and draftMode() — became asynchronous in Next.js 15, so every place that used to read params.slug directly now needs const { slug } = await params instead, and the official codemod handles most of this conversion automatically.

Why This Passes Locally and Fails Somewhere Else
Next.js's own version 15 upgrade guide is direct about the change: previously synchronous request-time APIs — params, searchParams, cookies(), headers(), draftMode() — are now asynchronous, specifically to separate prerender time from render time and enable more flexible caching patterns. The confusing part in practice is that this can look like it's still working on your own machine after the upgrade, while a genuinely clean build elsewhere fails outright. The usual reason is Next.js's generated route types living in .next/types — that folder doesn't automatically regenerate just because package.json now says version 15, and a dev server you haven't fully restarted, or a stale .next folder you haven't cleared, can keep quietly offering the old synchronous type shape. CI, a fresh clone, or a teammate pulling the branch cold all rebuild those types from scratch against the real new signature, and that's where the mismatch actually surfaces.
(If you've been trying to reproduce the CI failure locally by just running next build again — try deleting .next first. The stale generated types are often the entire gap between "works for me" and "fails in CI.")
The Codemod: Handles Most of the Migration Automatically
The direct fix, before touching anything by hand, is running Next.js's own migration tool:
1npx @next/codemod@canary upgrade latestThis upgrades the Next.js version itself and converts synchronous usage of params, searchParams, cookies(), headers(), and draftMode() to their async equivalents across the codebase. Next.js's full codemods reference documents every individual codemod available if you'd rather run a narrower one than the full upgrade command. Where it can't confidently make the conversion automatically, it inserts a comment or an UnsafeUnwrapped typecast flagging exactly where manual review is still needed — worth treating as a checklist rather than a finished migration.
The Manual Pattern, If You're Converting by Hand
For a page component and its generateMetadata function, the shape changes the same way in both places:
1// app/blog/[slug]/page.tsx — Before Next.js 15
2type Params = { slug: string };
3
4export function generateMetadata({ params }: { params: Params }) {
5 const { slug } = params;
6 return { title: slug };
7}
8
9export default async function Page({ params }: { params: Params }) {
10 const { slug } = params;
11 return <div>{slug}</div>;
12}1// app/blog/[slug]/page.tsx — After Next.js 15
2type Params = Promise<{ slug: string }>;
3
4export async function generateMetadata({ params }: { params: Params }) {
5 const { slug } = await params;
6 return { title: slug };
7}
8
9export default async function Page({ params }: { params: Params }) {
10 const { slug } = await params;
11 return <div>{slug}</div>;
12}
The pattern repeats identically for searchParams, and for Route Handlers receiving params as part of their second argument. generateMetadata is the piece most often missed during a manual migration — it's easy to update the page component's own signature and forget the metadata function sitting right beside it needs the exact same await treatment, since it reads from the same params object under a different function name.
Checking for Stale Generated Types Before Assuming the Migration Is Wrong
If code still passes locally after running the codemod and updating obvious usages, but a clean build elsewhere disagrees, clear the generated cache before debugging further:
1# Force Next.js to regenerate route types against the current version
2rm -rf .next
3npm run build
If the local build now fails the same way CI does, you've confirmed the discrepancy was stale generated types, not a genuine environment difference — the fix is in your code, and now you can see the actual errors instead of chasing a phantom "it works here" mismatch. Next.js's own error reference for this exact case is worth bookmarking, since it names the specific dynamic APIs affected if you're not certain which one is throwing.
The Opinion Part
Here's the position worth stating plainly: running the official codemod and calling the upgrade done, without a final rm -rf .next && npm run build to confirm against a genuinely clean state, is how a five-minute migration turns into a CI failure discovered days later. The codemod is very good at the mechanical conversion; it isn't a substitute for verifying against the same clean-build conditions your CI pipeline actually uses. Teams already spend an estimated 33–42% of their time servicing technical debt they didn't choose (Stripe Developer Coefficient) — a framework upgrade that "passed locally" but wasn't actually verified against a clean build is exactly the kind of debt that gets discovered at the worst possible moment, usually right before a deploy.
Conclusion
If Next.js 15's async params and searchParams are breaking production after an upgrade that seemed to go fine locally, run the official codemod first, then manually check generateMetadata functions specifically, since they're the most commonly missed piece. Before trusting that "it works locally" means the migration is complete, clear .next and rebuild clean — stale generated route types are the most common reason this passes on one machine and fails everywhere else.
If a static export is layered on top of this same upgrade, our dynamic server usage guide covers the separate set of constraints output: 'export' adds on top of everything covered here, and if NEXT_PUBLIC_ environment variables are also acting strangely since the upgrade, our Docker environment variables guide covers that unrelated but easy-to-conflate build-time issue.
Run the codemod, verify against a clean build, and get back to a params object that behaves the same way everywhere it's read — not just on the machine that happened to still have last week's cache.
Frequently Asked Questions
params and searchParams in page.js, layout.js, and route.js — along with cookies(), headers(), and draftMode() — changed from synchronous values to Promises that need to be awaited. Code that used to read params.slug directly now needs const { slug } = await params, because Next.js 15 separates prerender time from render time to enable more flexible caching and streaming.
The most common cause is Next.js's generated route types in .next/types, which don't automatically regenerate just because package.json now points at version 15 — a stale .next folder or a local dev server that hasn't been restarted can keep offering the old synchronous type shape, while a genuinely clean build (CI, a fresh clone, a teammate's machine) regenerates those types correctly against the new async signature and fails immediately.
Yes — running npx @next/codemod@canary upgrade latest handles both the version upgrade and the automated conversion of synchronous params, searchParams, cookies(), headers(), and draftMode() usage to their async equivalents. Where the codemod can't safely convert a specific usage automatically, it inserts a comment or an UnsafeUnwrapped typecast flagging exactly where manual review is still needed.
It's a temporary escape hatch the codemod inserts when it can't confidently convert a synchronous usage to async automatically — it lets the old synchronous access pattern keep working for now, while logging a warning in development to flag that it needs manual attention. It's meant as a stopgap during migration, not a long-term pattern; every UnsafeUnwrapped instance is worth converting to a proper await before considering the upgrade finished.
The same way — generateMetadata now receives params and searchParams as Promises too, and needs to be an async function that awaits them before reading any values. Missing this specific function during migration is a common gap, since it's easy to update the main page component's signature and forget the metadata function sitting right next to it needs the identical treatment.
