← All posts
·6 min read·GuardLayer

Fix: cookies() should be awaited (Next.js 15)

SupabaseNext.jsSessionsAuth

Route "/x" used cookies().get(...). cookies() should be awaited before using its value. means you're on Next.js 15+, where cookies() from next/headers returns a Promise. The fix is to make your Supabase createClient helper async, await cookies() once inside it, and await createClient() at every call site.

It fires per route, per render, in Server Components and Server Actions alike, and the obvious edits don't clear it — one thread on the Next.js repo is titled simply "Unsolvable". It isn't. It's a one-file change that has to be applied consistently, and partial application is what makes it look unfixable.

What changed in Next.js 15 — and what Next.js 16 did to it

cookies(), headers(), draftMode(), params and searchParams became asynchronous. Next.js 15 shipped a compatibility shim so the synchronous form still worked while logging this warning: a console full of warnings, an app that still rendered.

Next.js 16 removed that shim. Synchronous access to those APIs is gone, not deprecated — so the same code that merely warned on 15 fails on 16. If you're reading this because of the warning, you're on 15 and you have a window to fix it cleanly. If you're on 16 and hitting the hard failure instead, the fix below is the same one; Vercel also ships a codemod that does most of it:

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

The codemod awaits what it can and leaves a typecast or comment where it can't, so read its diff rather than trusting it wholesale.

The Supabase angle is that @supabase/ssr's createServerClient takes a cookies object with getAll and setAll methods, and nearly every tutorial written before Next 15 captures the cookie store synchronously at module scope. That pattern is exactly what the warning is pointing at.

How do I fix "cookies() should be awaited" with Supabase?

Make the helper async, await the store once, and close over it. One file. Leave that await cookies() uncaught, too — wrapping it in a try/catch swallows Next.js's own bailout signal and breaks the build with Route couldn't be rendered statically because it used cookies:

// utils/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // Called from a Server Component, which cannot write cookies.
            // Safe to ignore *only if* middleware is refreshing the session.
          }
        },
      },
    }
  );
}

Then every call site gains an await:

// Before
const supabase = createClient();

// After
const supabase = await createClient();

That second step is where people stall. Miss one call site and supabase is a Promise, so supabase.from(...) throws is not a function — an error that looks unrelated and sends you back to the cookie code. Find them all at once:

grep -rn "createClient()" --include=*.ts --include=*.tsx app/ lib/ | grep -v "await createClient"

Why the warning keeps firing after you "fixed it"

Three common causes:

  1. A second, older client helper. Most projects have utils/supabase/server.ts and something like lib/supabaseServer.ts left over from an earlier tutorial. Fix both, or delete one.
  2. cookies() called outside the helper. Route Handlers and Server Actions often call cookies() directly for an unrelated read. Those need awaiting too.
  3. @supabase/auth-helpers-nextjs. If your imports come from there rather than @supabase/ssr, you're on a deprecated package that will keep producing cookie errors no matter what you await. Migrating is the fix.

While you're in here, one adjacent cleanup that is not a cause of this warning: @supabase/ssr deprecated the per-cookie get/set/remove interface in favour of getAll/setAll. The old shape still works, but it emits its own deprecation warning from Supabase — easy to confuse with Next's, and worth migrating in the same pass.

The part that matters after the warning is gone

That empty catch in setAll is load-bearing, and it's worth understanding rather than copying.

Server Components cannot write cookies — by the time one renders, response headers may already be sent. So when Supabase rotates an access token during a Server Component render, cookieStore.set() throws, the catch swallows it, and the refreshed token is discarded. The render succeeds with a valid in-memory session, and the browser keeps the old cookie.

That's harmless if and only if something else is refreshing the session where cookies can be written. That something is middleware. Without it, the swallowed write becomes a session that silently stops refreshing until the access token ages out and users get a 401 with PGRST301: JWT expired at an arbitrary moment.

So the async-cookies migration has a second half:

(On Next.js 16.1+ this file is proxy.ts and the exported function is proxy — same code, renamed convention, npx @next/codemod@canary middleware-to-proxy handles it.)

// middleware.ts — the one place the session is actually refreshed.
export async function middleware(request: NextRequest) {
  // ... createServerClient wired to request/response cookies ...
  const { data: { user } } = await supabase.auth.getUser();

  if (!user && !request.nextUrl.pathname.startsWith("/login")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return response; // must be the response the cookies were written to
}

Two warnings while you're in here. First, getUser() — not getSession(). getSession() doesn't warn and doesn't await anything, which makes it an appealing thing to reach for mid-migration, but on the server it reads the cookie without verifying the JWT, turning your gate into a formality. Second, resolve the user in one place per request; a helper awaited in middleware and again in three parallel layouts can race its own refresh token.

Quick self-check

# Sync cookies() calls still lingering
grep -rn "cookies()" --include=*.ts --include=*.tsx app/ lib/ utils/ | grep -v "await cookies()"

# Call sites that forgot the await
grep -rn "= createClient()" --include=*.ts --include=*.tsx app/ lib/

# Still on the deprecated auth-helpers package?
grep -rn "@supabase/auth-helpers" package.json

Clean on all three and the warning is gone for good, rather than gone from the route you happened to reload.

FAQ

Why does Next.js 15 say cookies() should be awaited? Because cookies() returns a Promise as of Next.js 15. The framework shipped a compatibility shim so the synchronous form still ran, and this warning is that shim telling you it did.

Is this warning safe to ignore? No. Next.js 16 removed the shim entirely — synchronous access to cookies(), headers(), draftMode(), params and searchParams is gone. Code that only warns on 15 breaks on 16, so this warning is a preview of your next upgrade failing.

Do I have to make every page async? No. Server Components are already async-capable. You only need await where you call cookies() or your createClient() helper.

Why do I get "supabase.from is not a function" after the fix? A call site is missing its await, so supabase is still a Promise. Grep for = createClient() without await.

Should I use getSession() to avoid the async cookie code? No. getSession() doesn't verify the JWT on the server, so any authorization built on it can be spoofed with a crafted cookie. Await the cookies and keep getUser().

Why is the catch block in setAll empty? Because Server Components can't write cookies and Supabase attempts a write when it rotates a token. Swallowing that error is only safe if middleware refreshes the session where cookies can be set — otherwise sessions silently stop refreshing.

Catch this before it ships — free

GuardLayer scans every push for this and 28 other Next.js + Supabase issues, with the exact fix inline.

No signup, no card — your code is scanned in memory and never stored.

Keep reading

A Solvion project — see also Reglog — EU AI Act changelog, Proceedly, Solenna and Solvion Solutions.