TypeError cookies() crash in Next.js route handler
A production‑grade fix for the `TypeError: cookies() is not a function` crash that appears in Next.js route handlers after a deploy.
TL;DR#
If you're seeing TypeError: cookies() is not a function, the cause is usually that the cookies() helper is being called without await in a Next.js 15+ route handler. Fix it by adding await before cookies() and adjusting any downstream code that expects a cookie store.
If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.
What you'll see#
Error: TypeError: cookies() is not a function
at async /app/api/auth/login/route.ts:12:25
at processTicksAndRejections (node:internal/process/task_queues:96:5)
It happens when a POST request hits a route handler that reads or writes cookies after a recent deployment to Vercel. The behavior is the same across staging, production, and local npm run dev environments, which makes it easy to reproduce with a single curl command.
I first noticed this after merging a feature branch that introduced a new authentication endpoint. The GitHub issue that sparked this investigation is vercel/next.js#51156, where multiple teams reported intermittent 500 errors caused by the same stack trace. The error only appears after the code is built for production because the serverless runtime enforces the async contract of cookies() more strictly than the dev server.
Root cause#
In Next.js 15 the cookies() helper was refactored to be asynchronous. The function now returns a Promise<CookieStore> instead of a plain object. When you call cookies() without await, the variable you think holds a cookie store actually holds a Promise. The next line usually tries to read a cookie like cookieStore.get('session'), but because cookieStore is a Promise, JavaScript throws TypeError: cookies() is not a function. The error propagates up to the route handler, causing the entire request to fail with a 500 status.
Two things make this bug especially sneaky:
- The TypeScript compiler does not always flag the missing
awaitbecause the return type ofcookies()isanyin older type definitions. That means the code compiles cleanly, but at runtime the mismatch surfaces. - In the Vercel edge runtime, the handler is executed in a separate isolate where the Promise is never automatically resolved, so the error is not swallowed as it sometimes is in a Node.js dev server.
The relevant code path lives in the Next.js core, but the part that touches your code is the call site inside your route file. When you look at the stack trace, the topmost frame points to your file, not the framework, which is why the issue appears to be “your code”.
The relevant code path is:
// app/api/auth/login/route.ts
export async function POST(request: Request) {
const cookieStore = cookies(); // ← missing await
const body = await request.json();
if (body.password === 'secret') {
cookieStore.set('session', 'abc123', { httpOnly: true });
return new Response('Logged in', { status: 200 });
}
return new Response('Unauthorized', { status: 401 });
}
Notice how cookies() is called synchronously. The framework expects an async call, so the returned Promise is never resolved, and the subsequent set call triggers the TypeError.
The fix#
import { cookies } from 'next/headers';
// app/api/auth/login/route.ts
export async function POST(request: Request) {
// Await the async cookies helper
const cookieStore = await cookies();
const body = await request.json();
if (body.password === 'secret') {
// Now cookieStore is a proper CookieStore instance
cookieStore.set('session', 'abc123', { httpOnly: true });
return new Response('Logged in', { status: 200 });
}
return new Response('Unauthorized', { status: 401 });
}
That single change addresses the cause because it respects the async contract introduced in Next.js 15. The await forces the Promise to resolve before we interact with the cookie store, eliminating the TypeError. If you have multiple route handlers that use cookies(), apply the same pattern to each.
Step by step#
- Open the file that contains the failing route handler, e.g.,
app/api/auth/login/route.ts. - Locate the line that reads
const cookieStore = cookies();. - Prefix the call with
awaitso the line becomesconst cookieStore = await cookies();. - Ensure the surrounding function is declared
async(it already is in most cases). Save the file and runnpm run devor redeploy to Vercel. - If you use TypeScript, you may also want to update the import:
import { cookies } from 'next/headers';– this stays the same, but the type definitions will now correctly show aPromise<CookieStore>.
I wrote a companion guide that walks through the same pattern for the App Router in Next.js 15: "Fix "cookies() should be awaited" Error in Next.js 15 App Router (Complete Migration Fix 2026)". That article also explains why the lint rule next/no-unawaited-cookies is useful.
Verify the fix#
Run the following command against your local dev server:
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"password":"secret"}' -i
You should see output similar to:
HTTP/1.1 200 OK
set-cookie: session=abc123; Path=/; HttpOnly
Content-Type: text/plain; charset=UTF-8
Content-Length: 9
Logged in
Notice the set-cookie header is present and the status code is 200 instead of 500. If you still receive a 500 response, double‑check that every cookies() call in the file (and any imported helper) is awaited.
If you're still seeing the error, two common variants exist:
Variant A — cookies imported from the wrong package#
Some projects import cookies from next/headers while others mistakenly use next/cookies. The wrong import returns a stub that is not async, leading to a different error (cookies is not a function). Ensure your import looks exactly like this:
import { cookies } from 'next/headers';
If you see import { cookies } from 'next/cookies';, replace it with the correct path and re‑run the verification command.
Variant B — Using cookies() inside a middleware instead of a route handler#
Middleware runs in a different runtime where cookies() is still synchronous. If you copy the same code into middleware.ts, the await will cause a compile error. In that case, keep the call synchronous and use request.cookies directly. The fix for route handlers does not apply to middleware.
Why this happens (and how to avoid it next time)#
Next.js 15 introduced an async API surface for many header helpers to make them compatible with edge runtimes. The underlying invariant is that any helper that may need to read from a streaming request must be async. When the framework changed, the type definitions lagged, so developers kept using the old synchronous pattern. To avoid the regression:
- Add the ESLint rule
next/no-unawaited-cookiesto your.eslintrc.json. It will flag anycookies()call that lacksawait. - Write a unit test for each route handler that asserts the response contains the expected
set-cookieheader. A failing test will surface the missingawaitbefore you ship. - Pin your Next.js version to a minor release that you have verified, and read the migration guide for each major release. My guide on partial prerendering covers similar migration pitfalls: "Next.js 15 Partial Prerendering: The Complete Production Guide".
By treating the async nature of cookies() as a first‑class citizen, you prevent the TypeError from resurfacing after future upgrades.
Related#
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
Next.js Authentication Comparison 2026: Clerk vs Better Auth vs Supabase vs Auth.js
We tested all four major auth solutions across 50+ real-world scenarios in production. Here is the honest comparison nobody else gives you — including the middleware vulnerability that changed everything, migration costs, and which one actually scales.
Debugging Supabase RLS Issues: A Step-by-Step Guide
Master RLS debugging techniques. Learn how to identify, diagnose, and fix Row Level Security policy issues that block data access in production.
Fix Supabase Auth Session Not Persisting After Refresh
Supabase auth sessions mysteriously disappearing after page refresh? Learn the exact cause and fix it in 5 minutes with this tested solution.
Browse by Topic
Find stories that matter to you.
