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.
