Fix "cookies() should be awaited" Error in Next.js 15
Next.js 15 broke synchronous `cookies().get()`. Every server-side call must now `await cookies()` first. Here's the precise migration — App Router pages, route handlers, Server Actions, and Supabase SSR — plus the codemod that fixes 90% of call sites automatically.
Upgraded to Next.js 15 and now every page console-warns about cookies? You missed one of the biggest breaking changes in the App Router. Two-minute fix.
The problem#
After next@15, the dev server logs:
Route "/dashboard" used `cookies().get('sb-access-token')`. `cookies()` should
be awaited before using its value. Learn more:
https://nextjs.org/docs/messages/sync-dynamic-apisThe page still renders (in 15 the API is sync-compatible with a warning), but in Next.js 16 this turns into a runtime error. Same warning shape applies to headers() and draftMode().
Symptoms#
- Console warnings on every server-rendered request mentioning
sync-dynamic-apis. - The route works locally but you suspect it'll break on upgrade.
- Supabase SSR throws "Cookies can only be modified in a Server Action or Route Handler" (a related but distinct issue, also fixed below).
- TypeScript errors after
npm i next@15:Property 'get' does not exist on type 'Promise<ReadonlyRequestCookies>'.
Root cause#
Up to Next.js 14, cookies(), headers(), and draftMode() returned values synchronously. To support streaming Server Components without forcing Next.js to materialize the request before render starts, the React team made these APIs thenable in 15. Calling .get() on a Promise hits the synchronous compatibility shim that warns you to migrate.
This isn't a bug — it's a deliberate semantic change. The migration path is await before the method call.
Fix — Server Component#
Before:
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default function DashboardPage() {
const cookieStore = cookies(); // ⚠️ sync
const token = cookieStore.get('sb-access-token')?.value;
// ...
}After:
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function DashboardPage() {
const cookieStore = await cookies(); // ✅ awaited
const token = cookieStore.get('sb-access-token')?.value;
// ...
}The page function becomes async. Server Components support async natively — no wrapper, no use() hook.
Fix — Route Handler#
// app/api/me/route.ts
import { cookies, headers } from 'next/headers';
export async function GET() {
const cookieStore = await cookies();
const headersList = await headers();
const token = cookieStore.get('sb-access-token')?.value;
const userAgent = headersList.get('user-agent');
return Response.json({ token: Boolean(token), userAgent });
}Route handlers were already async — just add the await.
Fix — Server Action#
'use server';
import { cookies } from 'next/headers';
export async function logout() {
const cookieStore = await cookies();
cookieStore.delete('sb-access-token');
}Server Actions are always async. Same pattern.
Fix — Supabase SSR (createServerClient)#
This is where most upgrades break silently. The Supabase cookies option needs async getters:
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// setAll was called from a Server Component (read-only).
// This is expected when refreshing the session in a Server
// Component — middleware will refresh it on the next request.
}
},
},
}
);
}Two things matter:
createClientis itself async — the factory awaits cookies once, then the inner closures use the resolved store synchronously.setAllswallows the "read-only" throw. Server Components can't write cookies; that throw is expected and your middleware handles refresh on the next request anyway.
Then every call site:
// Before
const supabase = createClient();
// After
const supabase = await createClient();Run the codemod#
npx @next/codemod@latest next-async-request-api .It rewrites simple sync call sites automatically:
cookies()→await cookies()(and marks the enclosing functionasync)- Same for
headers()anddraftMode() - Adds the necessary
asynckeywords up the call chain when safe
What it won't fix:
- Custom factories like the Supabase
createClientabove - Sync utility functions that internally call
cookies()and are themselves called from non-async contexts - Cases where the function signature change would be a public API break
Review the diff, run npm run typecheck, fix the remaining ~10% by hand.
Verification#
# 1. Type-check passes
npm run typecheck
# 2. Dev server logs no sync-dynamic-apis warnings
next dev
# Hit a few routes, watch the terminal:
# Expected: no warnings
# Bad: "Route X used cookies().get()..."
# 3. Build succeeds with the eslint rule enabled
next buildAdd this ESLint rule to prevent regressions:
// eslint.config.mjs
export default [
{
rules: {
'@next/next/no-sync-scripts': 'error',
// Next.js 15 ships an internal rule that catches sync cookies/headers
// in App Router files. Enable it explicitly:
'next/no-sync-dynamic-apis': 'error',
},
},
];Debug checklist#
- Is the call site
await-ed? Most warnings are literally one missing keyword. - Is the enclosing function
async? If TypeScript complains aboutawaitoutside async, that's the cause. - Does
awaitwork in your Server Component? It must — if you see "await is not allowed here," you accidentally put it in a Client Component ('use client'). - For Supabase SSR: did you upgrade
@supabase/ssrto0.5+? Older versions don't support the async pattern cleanly. - After migrating, are you accidentally fetching cookies twice per render?
await cookies()is cheap but not free — destructure once at the top.
Prevention#
- Pin Next.js minor versions until you've migrated. Auto-updates that cross a major bump bite teams that don't watch the release notes.
- Read the upgrade guide before bumping — Next.js publishes a per-version migration page. Five minutes of reading saves an hour of debugging.
- Adopt the lint rule so new code can't reintroduce the sync pattern.
- Wrap framework primitives behind your own factories where you can — when the next API change lands, you change one file.
The sync-to-async migration is one of the cleanest breaking changes Next.js has ever shipped. Codemod + a half-hour cleanup and you're done.
Related reading#
- Hub: Next.js + Supabase: The Complete Resource Hub
- Next.js App Router Folder Structure at Scale
- Supabase Auth Complete Session Middleware Guide
- Next.js + Supabase SSR Session Management
- Next.js Hydration Mismatch Error: Exact Fixes for App Router and React 19
- How to get query string parameters in Next.js
- Next.js Redirect from / to another page
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Fix Next.js revalidatePath Not Working in Server Actions
Your Server Action mutates data but the page shows stale values until you hard-refresh. `revalidatePath` is one of those APIs that "succeeds" while doing nothing. Here are the six reasons it no-ops, with the exact fix for each — including the one nobody tells you about: `dynamic = 'force-static'`.
Vercel Next.js Environment Variables: Fix & Deploy (2026)
Environment variables not working on Vercel? Learn the exact configuration needed for Next.js 15 deployment with zero errors.
Fix Next.js "Module Not Found" After Deploy or Build
Module not found errors only in production? Learn why Next.js builds fail after deploy and get 6 proven fixes that work on Vercel, AWS, and other platforms.
Browse by Topic
Find stories that matter to you.
