Skip to content

Next.js error

`cookies()` Returns a Promise and Must Be Unwrapped

The error

Route "/account" used `cookies().get(...)`. `cookies()` returns a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties.

The request APIs became asynchronous. Awaiting them is the fix, but the call site you await at decides whether the route stays static, so the mechanical change has a rendering consequence.

The message names the route and the call:

Route "/account" used `cookies().get(...)`. `cookies()` returns 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

cookies(), headers() and draftMode() all read the incoming request, and all three became asynchronous together.

The fix

// before
import { cookies } from 'next/headers'
 
export default function Account() {
  const token = cookies().get('session')?.value
}
 
// after
export default async function Account() {
  const jar = await cookies()
  const token = jar.get('session')?.value
}

Await once and keep the result. A codebase that writes (await cookies()).get(...) three times in one component works and reads badly, and the third one is where somebody eventually drops the parentheses.

In a Route Handler and a Server Action the change is identical, because both were already async.

The part that is not mechanical

Where you await decides what the rest of the page can do while it waits.

// everything below this line waits for the request read
export default async function Page() {
  const jar = await cookies()
  return (
    <>
      <ExpensiveStaticSection />
      <Greeting token={jar.get('session')?.value} />
    </>
  )
}

Hoisting the await to the top of a component is the obvious translation of the old synchronous code, and it makes the whole subtree wait on it. Pushing the read down into the component that needs it, behind its own boundary, lets the rest render first:

export default function Page() {
  return (
    <>
      <ExpensiveStaticSection />
      <Suspense fallback={<GreetingSkeleton />}>
        <Greeting />          {/* awaits cookies() itself */}
      </Suspense>
    </>
  )
}

That is the reason these became promises rather than a change for its own sake. The mechanical fix is correct and finishes the upgrade; this is the version that gets something out of it.

When the route does not look like it reads cookies

The message names the route, and the read is frequently several files away - an auth helper, a locale resolver, an experiment flag read inside a shared layout. Everything rendered under that layout inherits it.

Searching for cookies( across the repository finds it faster than reading the route. If the read turns out to live in a layout that wraps the whole application, that one call is why nothing is static any more, and it is worth asking whether it belongs there.

The other half of the same change

Route parameters moved at the same time and produce a nearly identical message. The codemod that handles one handles the other: npx @next/codemod@latest next-async-request-api .

Both are standard work in a version migration, and both are quick. The part that takes judgement is deciding, for each read, whether to keep the await where the old code had it or move it to where it actually matters.

Related questions

Does `headers()` change the same way?
Yes, and so does `draftMode()`. They are the same family and they moved together, so a codebase that reads all three will produce three versions of this message in whatever order the routes happen to render.
Can we call these in a Client Component now that they return promises?
No. These read the incoming request, and a Client Component has no request to read - it may be running in a browser long after the response was sent. The error you get there is a different one about importing `next/headers` in a client module, and it is telling the truth.
Does awaiting them make the route dynamic?
Reading them does, and that was true before they were promises. The await is not what opts the route out of static rendering; touching request data is. What the promise changes is where you can put that read without blocking everything else on the page.
Why did an upgrade break a route that does not read cookies?
Something it imports probably does. An auth helper or an analytics wrapper called from a layout reads the request on behalf of every page under it, and the message names the route rather than the file, which is what makes it confusing.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page