The patch is two edits per call site: change const store = cookies() to
const store = await cookies(), and mark the enclosing function async. That
is the entire fix for cookies() should be awaited — the rest of this page is
the fast diagnostic (find every call site), the minimal patch patterns for each
App Router surface, and the verification pass that catches the call site you
missed.
If you upgraded a Next.js app to 15 or 16 and your logs suddenly filled with
Error: Route "/dashboard" used cookies().get(...). cookies() should be awaited, nothing in your code is broken in the way the message
before using its value
suggests. Next.js changed the contract of cookies() — and the same change
applies to headers(), draftMode(), and the params/searchParams props
passed to pages and layouts. This page is the express repair; for the full
migration story — why the API went async, the next-async-request-api codemod,
Client Component React.use() unwrapping, and the Supabase createServerClient
factory pattern — see the companion deep dive:
Fix "cookies() should be awaited" Error in Next.js 15.
What actually changed
In Next.js 14, cookies() from next/headers returned a ReadonlyRequestCookies
object synchronously. You could call .get() on it immediately. In Next.js 15
the function became asynchronous: it now returns a Promise, and you have to
await it before touching .get(), .getAll(), .has(), or .set().
This was a deliberate move. Making these request-scoped APIs async lets the
framework start rendering the static shell of a route before the dynamic,
per-request data (cookies, headers) is resolved. It is the groundwork for
Partial Prerendering. The catch is that the codemod does not always reach every
call site, and a synchronous .get() on the returned promise fails in a way
that looks like an auth bug rather than a syntax change.
Why it looks like a session bug
In Next.js 15, un-awaited cookies().get(...) mostly still works: the docs
say sync access is kept "to help with backwards compatibility", so a shim
answers the call and logs the warning instead of crashing. That compatibility is
exactly what makes the bug sneaky — the app appears fine while every request
spams the log, and the access patterns the shim does not cover (spreading,
iterating, passing the store around before it resolves, and TypeScript treating
the return value as Promise<ReadonlyRequestCookies>) surface as undefined
session values instead of a clean error. A session lookup that returns null
means middleware treats the user as logged out, and you get redirects to
/login or empty SSR pages even though the cookie is present in the request.
That is why this shows up as "auth broke after the upgrade" rather than a syntax
error — and why the sync grace period is a migration window, not a fix.
The fix
Await the call, and make the enclosing function async:
// ❌ Next.js 14 pattern — throws in 15/16
import { cookies } from "next/headers";
export function getToken() {
return cookies().get("sb-access-token")?.value;
}
// ✅ Next.js 15/16
import { cookies } from "next/headers";
export async function getToken() {
const cookieStore = await cookies();
return cookieStore.get("sb-access-token")?.value ?? null;
}
The single most effective change is to centralize cookie access in one helper
and await it there. Mixed patterns — one route awaits, another does not — are the
hardest version of this bug to track down, because half your app works.
// lib/session.ts
import { cookies } from "next/headers";
export async function readSessionCookie(name: string) {
const store = await cookies();
return store.get(name)?.value ?? null;
}
Every server component, route handler, and server action then calls
await readSessionCookie(...) and there is exactly one place that knows the API
is async.
If a call site truly cannot be made async today, Next.js 15 documents a typed
escape hatch that keeps the sync behavior while still warning in dev:
import { cookies, type UnsafeUnwrappedCookies } from "next/headers";
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies;
const token = cookieStore.get("sb-access-token")?.value;
The name is the documentation: it is unsafe, temporary, and exists only for
backwards compatibility. Treat every occurrence as an open migration ticket.
Writing cookies
.set() follows the same rule, but it can only be called from a Server Action
or a Route Handler — not from a server component during render. If you try to
set a cookie inside a page component you will get a different error about mutating
cookies during rendering. Move the write into an action:
"use server";
import { cookies } from "next/headers";
export async function login(token: string) {
const store = await cookies();
store.set("sb-access-token", token, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
});
}
Middleware is the exception
Inside middleware.js you do not use next/headers. You read cookies from
the request object, and that API stayed synchronous:
export function middleware(request) {
const token = request.cookies.get("sb-access-token")?.value;
// ...
}
Mixing the two — awaiting in a server component but expecting the middleware API
to behave the same — is a common source of "it works in the route but not the
redirect" confusion.
Verifying the fix
Do not trust a clean dev console alone; the async gap can still silently return
null in a code path the compiler cannot check. Verify behaviorally:
-
grep -rn "cookies()\." src appand confirm every hit is preceded byawait. - Run a production build (
next build && next start), log in, and hard-refresh a server-rendered protected route. The session must survive the refresh. - Check the server logs during that refresh — a lingering
cookies() should be awaitedline means a call site was missed.
A missed call site in a rarely-hit route is exactly how this reaches production,
so the grep-plus-refresh pass matters more than the passing build.
Related Incidents
- INC-001 cookies() warning
- Next.js Hydration Mismatch: 8 Fixes for App Router (2026)
- Next.js Server Actions Debugging Matrix: Production Fix Matrix
- Fix Next.js revalidatePath Not Working in Server Actions
- Next.js Scroll Resets When searchParams Change
- TypeError: cookies() crash in a Next.js route handler — the same async contract, one surface later: once the warning is fixed in Server Components, the route handlers that still destructure the return value throw outright.
Originally published at https://www.iloveblogs.blog
Top comments (0)