DEV Community

Cover image for TypeError cookies() crash in Next.js route handler
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

TypeError cookies() crash in Next.js route handler

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)
Enter fullscreen mode Exit fullscreen mode

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.

  • Symptom: TypeError: cookies() is not a function in a route handler
  • Root cause: cookies() is async in Next.js 15/16 and must be awaited
  • Fix: Add await to the call and update dependent code
  • Verification: Run the handler locally or with curl and see a 200 response instead of 500

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:

  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.

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 });
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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:

  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.

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"}' -i
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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';
Enter fullscreen mode Exit fullscreen mode

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:

  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.

Related


Originally published at https://www.iloveblogs.blog

Top comments (0)