Next.js 16 Async Params Broke Everything (In a Good Way)
Every dynamic route now awaits its params. Once you internalize it, it feels right.
Next.js 16 shipped with a breaking change that sounds small and isn't: dynamic route params are now Promises. You can't access them synchronously anymore. Every page, every generateMetadata, every route handler that touches params has to await them.
When I first hit this, I was annoyed. By week two, I was convinced it was the right call. Here's why.
The old model
Next.js 14 and earlier:
export default function Page({ params }: { params: { slug: string } }) {
const { slug } = params;
// use slug
}Synchronous. Obvious. Looks like every other component.
The new model
Next.js 16:
type PageProps = { params: Promise<{ slug: string }> };
export default async function Page({ params }: PageProps) {
const { slug } = await params;
// use slug
}Async. The function has to be async. You have to await params before destructuring.
Same for generateMetadata. Same for route handlers. Everywhere.
What broke
Every dynamic route in my existing codebase broke in the same way: TypeScript errors about params being a Promise where I was expecting an object. About fifteen locations in my code, all fixable by adding await and changing the type annotation.
The total migration took about 45 minutes. Not terrible. But it was 45 minutes I wasn't planning to spend.
The harder break was muscle memory. For the first couple days, I kept writing the old sync pattern and TypeScript would yell at me. By day three, the new pattern was automatic.
Why Next.js did this
Async params aren't about user experience. They're about streaming. In the old model, a page component couldn't render until its params were resolved, which limited Next's ability to start streaming the response.
In the new model, the rest of the page can start rendering — HTML, layouts, static parts — while params resolve in parallel. It's a small performance gain per request, but it unlocks bigger streaming optimizations down the line.
This is the kind of breaking change that pays off later, not immediately. Next.js is betting that future framework improvements will depend on async params. They're probably right.
Why I came to like it
After the initial pain, I realized async params force a cleaner mental model.
Params are input to the page. Input comes from outside the component. Outside means async. Every other async input — database calls, API fetches, headers — is already awaited. Params being synchronous was actually the anomaly.
Now the whole component is consistent. Every input is async. Every consumption is awaited. The code reads like a coherent flow instead of a mix of sync and async patterns.
The pattern I use now
Every page component with params looks like this:
type PageProps = { params: Promise<{ slug: string }> };
export default async function Page({ params }: PageProps) {
const { slug } = await params;
// rest of the component
}It's five extra characters compared to the old pattern. Muscle memory handled it within a week. My codebase is cleaner. The framework is happier.
The migration checklist
If you're migrating, here's what to touch:
- Every
app/*/page.tsxwith[dynamic]route segments. - Every
app/*/layout.tsxwith dynamic segments. - Every
app/*/route.tswith dynamic segments. - Every
generateMetadatathat uses params. - Every
generateStaticParams— no change needed, but check you're not accidentally awaiting something that doesn't need it.
TypeScript will flag every location if your tsconfig is strict. Run the build and fix errors one by one.
What I learned
Breaking changes in frameworks are always annoying when they happen and almost always right a year later. The Next.js team has been running this playbook for years — App Router, server components, Turbopack — and each migration felt painful at the time and correct in retrospect.
Pay the migration cost. Stay on the latest. The cost is front-loaded and the benefit compounds.
I used to fight framework upgrades. Now I just do them the week they ship. Smaller upgrades, more frequent, less pain per upgrade. The opposite strategy — batching upgrades for years and then doing a massive migration — has failed me enough times that I'm cured of it.
Upgrade early. Upgrade often. Accept the friction. The future version of your codebase will thank you.