Next.js error
`params` Is a Promise and Must Be Unwrapped
The error
Route "/blog/[slug]" used `params.slug`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties.Dynamic route parameters became asynchronous. One keyword fixes a Server Component, a different hook fixes a Client Component, and generateStaticParams is the exception.
The message names the route and the exact property you touched:
Route "/blog/[slug]" used `params.slug`. `params` is a Promise and must be
unwrapped with `await` or `React.use()` before accessing its properties.
Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis
Nothing is broken in your logic. The shape of one argument changed, and the error is precise about which one.
In a Server Component, await it
This is the common case and the whole fix is one keyword:
// before
export default function Page({ params }: { params: { slug: string } }) {
const post = getPost(params.slug)
}
// after
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = getPost(slug)
}The type changes too. Leaving it as the old shape means TypeScript now disagrees with the runtime, and it will be right.
In a Client Component, use use()
A Client Component cannot be async, so there is no await to add. React's use hook unwraps the promise instead:
'use client'
import { use } from 'react'
export default function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = use(params)
}The client-side form of the message is worded differently - it begins "A param property was accessed directly" and mentions only React.use(), because await was never available to you there.
The one place it stays synchronous
generateStaticParams returns the parameters; it does not receive them. It is unchanged, and it still deals in plain objects:
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug })) // not a promise
}Teams converting a route file top to bottom sometimes "fix" this one too and then spend an afternoon on why every path is missing.
Where the codemod leaves work
Run npx @next/codemod@latest next-async-request-api . first. It handles the obvious shapes and saves most of the typing.
What it reliably misses is a parameter that leaves the component before it is read. If a page passes params into a helper, or into a function that is not async, the access happens somewhere the codemod cannot see the route from:
// the codemod will not follow this
function buildCanonical(params: { slug: string }) { … }
export default async function Page({ params }) {
const canonical = buildCanonical(await params) // await at the boundary
}Await at the boundary and pass the resolved object onward. Passing the promise down and awaiting it in four different helpers works and gives you four places to get it wrong later.
After the codemod, search for params and searchParams across the diff rather than trusting it. The ones it missed are the ones that only appear when that route is requested, which may be a route nobody opened during the upgrade.
The same change, elsewhere
cookies(), headers() and draftMode() moved at the same time and produce their own version of this message. If you are partway through an upgrade, expect to meet both.
This is routine work in a version migration, and the whole of it is mechanical. What is not mechanical is finding the routes that nobody exercised, which is why the search through the diff matters more than the codemod run.
