Next.js cookies() Async TypeError After Upgrade: Fix

Next.js cookies() throwing TypeError after upgrading to 15 or 16? Fix the async API change in Server Components, Server Actions, and Route Handlers fast.
Next.jsReactApp Router
April 25, 20265 min read983 words

The Problem

Bumped a client project from Next.js 14.2 to 16.2 last weekend, ran the build, and the entire authenticated dashboard crashed with TypeError: cookies(...).get is not a function. Running locally, the dev overlay flagged it as a runtime error in the layout. The same code ran fine on 14.2 the day before.

If you just upgraded and you are seeing any of these errors, this post is for you:

  • TypeError: cookies(...).get is not a function
  • Error: Route /dashboard used cookies inside ... cookies should be awaited before using its value.
  • cookies().get is not a function during prerender
  • headers() is no longer synchronous in Sentry

Same root cause for all four. The dynamic data APIs went async in 15, and anything still calling them synchronously hits a hard error in 16.

Why It Happens

In Next.js 14 and earlier, cookies(), headers(), draftMode(), params, and searchParams all returned synchronous values. You could write cookies().get('session') directly. The runtime intercepted those calls and tracked them as dynamic dependencies.

In 15 these became async. The functions now return Promise<ReadonlyRequestCookies>, params and searchParams are passed as promises to your Server Components, and the synchronous access path was preserved as a deprecation path with a console warning.

In 16, the deprecation path is gone. Calling cookies().get(...) synchronously returns a Proxy that no longer implements .get, hence the is not a function error. The old synchronous behavior was kept alive in 15 specifically so the codemod could run, but the codemod only patches what it can statically detect. Anything indirected through a helper, a custom hook, or a third-party library slips through.

There is also a second flavor of this bug. When you destructure params in a generateMetadata function or a page component, TypeScript happily accepts the old shape if your next types are stale. The build fails at runtime because params is now a Promise but your code treats it as the raw object.

The Fix

Step 1: Run the official codemod first. This catches roughly 80% of call sites:

npx @next/codemod@canary next-async-request-api .

Re-run your build after. If it still fails, you have call sites the codemod missed; keep going.

Step 2: Audit every direct cookies() and headers() call. Grep the project:

grep -rEn "cookies\(\)\.(get|set|delete|has|getAll)|headers\(\)\.(get|has|forEach)" \
  --include="*.ts" --include="*.tsx" .

For each match, the fix is either await or destructure. In a Server Component or Server Action:

// Before (Next.js 14 style)
import { cookies } from 'next/headers'

export default function Dashboard() {
  const session = cookies().get('session')?.value
  return <div>Hi {session}</div>
}

// After (Next.js 15+/16)
import { cookies } from 'next/headers'

export default async function Dashboard() {
  const cookieStore = await cookies()
  const session = cookieStore.get('session')?.value
  return <div>Hi {session}</div>
}

The component itself has to be async. If it was a sync function before, the conversion is just adding async and one await.

Step 3: Update every page and layout that destructures params or searchParams.

// Before
type Props = {
  params: { slug: string }
  searchParams: { tab?: string }
}

export default function Page({ params, searchParams }: Props) {
  return <Article slug={params.slug} tab={searchParams.tab} />
}

// After
type Props = {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ tab?: string }>
}

export default async function Page({ params, searchParams }: Props) {
  const { slug } = await params
  const { tab } = await searchParams
  return <Article slug={slug} tab={tab} />
}

The same change applies to generateMetadata, generateStaticParams callers, and any custom helper that took params as an argument.

Step 4: Patch helpers that wrap the dynamic APIs. If you have an auth helper like this, convert it:

// lib/session.ts
import { cookies } from 'next/headers'
import { verify } from './jwt'

export async function getSession() {
  const cookieStore = await cookies()
  const token = cookieStore.get('session')?.value
  if (!token) return null
  try {
    return await verify(token)
  } catch {
    return null
  }
}

Then update every caller. If you used to write const user = getSession(), it now needs const user = await getSession(). The TypeScript compiler will flag the mismatch as long as your next types are current, so before you start fixing call sites, run npm i -D next@latest typescript@latest and npx tsc --noEmit to get a clean list of broken usages.

Step 5: Watch out for the third-party libraries. Some auth libraries (NextAuth v4, Clerk pre-5.x, several custom Supabase wrappers) call cookies() internally. If your error stack trace points at node_modules/..., you need to upgrade the library, not your code. NextAuth v5 (now Auth.js) handles this correctly; the v4 release line will not be patched.

For full migration details, the Next.js async request APIs upgrade guide walks through every API in detail.

The Lesson

The cookies() async migration is one of those upgrades where the codemod gets you 80% of the way and the last 20% requires a grep, a tsc --noEmit, and patience. Convert every direct call site, then patch the helpers, then upgrade any library that still calls the sync API internally. Do not skip the tsc --noEmit step; runtime errors here will only show on routes you actually load.

If you are stuck mid-upgrade and need someone to drive the migration, this is the kind of work I do as a fixed-scope engagement — see my services, or if you are also seeing async params errors, I covered that one in Next.js async params TypeError on dynamic routes.

Back to blogStart a project