TypeError cookies() crash in Next.js route handler
Technology

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.

2026-05-27
7 min read
TypeError cookies() crash in Next.js route handler

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:

  1. The TypeScript compiler does not always flag the missing await because the return type of cookies() is any in older type definitions. That means the code compiles cleanly, but at runtime the mismatch surfaces.
  2. 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:

js
// 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#

js
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#

  1. Open the file that contains the failing route handler, e.g., app/api/auth/login/route.ts.
  2. Locate the line that reads const cookieStore = cookies();.
  3. Prefix the call with await so the line becomes const cookieStore = await cookies();.
  4. Ensure the surrounding function is declared async (it already is in most cases). Save the file and run npm run dev or redeploy to Vercel.
  5. 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 a Promise<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:

bash
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:

js
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:

  1. Add the ESLint rule next/no-unawaited-cookies to your .eslintrc.json. It will flag any cookies() call that lacks await.
  2. Write a unit test for each route handler that asserts the response contains the expected set-cookie header. A failing test will surface the missing await before you ship.
  3. 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.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.