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.
Photo by Jake Walker on Unsplash
A route handler that reads or writes cookies starts returning 500s right after a deploy to Vercel, and the logs show this stack trace:
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)The short version: in Next.js 15+ the cookies() helper is asynchronous. Call it without await and the variable you think holds a cookie store actually holds a Promise — the next .get() or .set() blows up with this TypeError. Adding await in front of cookies() (and updating any downstream code that expects a cookie store) resolves it.
The stack trace blames your file, not the framework#
The error surfaces 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.
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”.
What changed: cookies() now returns a Promise#
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.
Here is what the failing call site looks like:
// 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 one-line change: await cookies()#
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:
- 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.
Prove it with a curl request#
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"}' -iYou 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 inNotice 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.
Still crashing? Two look-alike causes#
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.
cookies() called from 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 Next.js made header helpers async#
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#
- cookies() Should Be Awaited in Next.js 15/16: Exact Production Fix Guide
- Fix "cookies() should be awaited" Error in Next.js 15 App Router (Complete Migration Fix 2026)
- Next.js 15 Partial Prerendering: The Complete Production Guide
- Window is not defined in Next.js – 2026 Fix for React Apps
- NextJS Warning: Extra attributes from the server – Fix 2026
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 React 18 Hydration Mismatch in Next.js
A step‑by‑step fix for the React 18 hydration mismatch error in Next.js apps, covering root cause, code changes, verification, and prevention.
Fix "lcp" not working in production
When LCP data never appears in your analytics, a missing reportWebVitals export is usually to blame. Follow these steps to fix it.
How to implement LCP end to end
Learn how to instrument Largest Contentful Paint (LCP) in a Next.js project, send the metric to your analytics provider, and verify that the data is accurate.
Browse by Topic
Find stories that matter to you.
