Skip to content

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.

Related questions

Does this also apply to `searchParams`?
Yes, and the message is the same sentence with `searchParams` in place of `params`. Both arrive as promises on a page, and both are awaited the same way. A layout receives `params` but never `searchParams`, which is worth knowing before you go looking for it.
Can we silence it instead of fixing it?
In the versions where it was a warning you could, and the ones where it throws you cannot. Silencing it was never worth it anyway: the warning is accurate, the property is genuinely not there yet, and the value you read while ignoring it is `undefined` rather than wrong in some interesting way.
Why was this changed at all?
Because the framework can start rendering a route before it knows every parameter, and a promise is how it says so. Making these values awaitable is what lets the parts of a page that do not depend on them stream first.
Is there a codemod?
Next ships one, and it handles the common shapes - the `await` in an async component, the type change, the straightforward destructure. It will not fix a parameter read inside a non-async helper that you call from a component, so run it and then search the diff for what it left.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page