Skip to content

Fix: Next.js 'params should be awaited before using its properties'

FixDevs ·

Quick Answer

How to fix Next.js 15 async params and searchParams errors — await in Server Components, React.use in Client Components, generateMetadata, generateStaticParams, and the codemod migration path.

The Error

You upgrade to Next.js 15 and your dynamic route logs a runtime error:

Error: Route "/posts/[slug]" used `params.slug`. `params` should be awaited before using its properties.

Or the same error pattern for searchParams:

Error: Route "/search" used `searchParams.q`. `searchParams` should be awaited before using its properties.

Your code looks innocent — it’s the same shape that worked in Next 14:

// app/posts/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
  return <h1>{params.slug}</h1>;
}

In dev, you get a runtime warning. In production builds, the same access throws.

Why This Happens

Next.js 15 changed the contract for dynamic APIs. params, searchParams, cookies(), headers(), and draftMode() are all asynchronous now. They return Promises (or have promise-like access patterns), and synchronous property reads are deprecated — Next intentionally throws or warns to force you to migrate.

The reason isn’t arbitrary. Making these APIs async lets Next.js render the static parts of a page before the dynamic parts are known, then stream the dynamic shell once cookies/params resolve. The old sync API blocked rendering until everything was available.

Two consequences:

  • Server Components must await params. They run in an async context, so this is straightforward.
  • Client Components can’t await at the top level, so they use React.use(params) instead. React.use is a Suspense-aware hook that unwraps a Promise.

The error message is identical in both cases but the fix differs by component type.

Fix 1: Server Components — await the Params

If your component is a Server Component (the default in the App Router), make it async and await:

// app/posts/[slug]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <h1>{slug}</h1>;
}

Three things changed:

  1. The component is async.
  2. The params type is Promise<{ slug: string }>, not { slug: string }.
  3. You destructure after awaiting.

The same pattern works for searchParams:

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q?: string; page?: string }>;
}) {
  const { q, page = "1" } = await searchParams;
  return <SearchResults query={q} page={page} />;
}

Pro Tip: Destructure once at the top of the function and pass the plain values down. Don’t pass the params Promise into child components — await once and treat the result as a normal object.

Fix 2: Client Components — Use React.use()

Client Components run in the browser and can’t be async. Unwrap the Promise with React.use:

"use client";

import { use } from "react";

export default function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = use(params);
  return <h1>{slug}</h1>;
}

use(params) suspends the component until the Promise resolves. Next.js wraps the Client Component in Suspense automatically, so this Just Works during streaming.

For Client Components that read searchParams reactively (URL changes without a full navigation), prefer useSearchParams() from next/navigation — it’s reactive, where use(params) reads the value once at render:

"use client";

import { useSearchParams } from "next/navigation";

export default function SearchBar() {
  const sp = useSearchParams();
  return <p>Query: {sp.get("q")}</p>;
}

Common Mistake: Calling React.use inside a conditional or after an early return. use follows the rules of hooks — call it at the top level of the function, before any branching.

Fix 3: generateMetadata Also Receives Async Params

If you have a generateMetadata export, its params argument is also a Promise now:

import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetchPost(slug);
  return {
    title: post.title,
    description: post.excerpt,
  };
}

The same applies to layout generateMetadata, generateViewport, and route handler context ({ params } in route.ts):

// app/api/posts/[id]/route.ts
import { NextResponse } from "next/server";

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const post = await fetchPost(id);
  return NextResponse.json(post);
}

Fix 4: generateStaticParams Stays Synchronous (Mostly)

generateStaticParams itself is unchanged — it returns an array of param objects, just like before:

export async function generateStaticParams() {
  const posts = await fetchAllSlugs();
  return posts.map((p) => ({ slug: p.slug }));
}

The return value is the same shape. The async access happens at the page level, not in generateStaticParams. Don’t wrap the return values in Promises — Next.js does that for you when it constructs the params for each page.

Fix 5: Use the Codemod for Bulk Migration

Next.js ships a codemod that rewrites params usage across your project:

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

It:

  • Adds async to Server Components that read params or searchParams.
  • Inserts await params before property access.
  • Wraps Client Component reads with React.use() and adds the import.
  • Updates type annotations to Promise<...>.

Run it with a clean git working tree, then review the diff. The codemod is good but not perfect — it leaves a few edge cases (deeply destructured params, params passed through HOCs) for you to fix by hand.

Note: If you skipped the codemod and have a large project, audit with grep -r "params\." app/ to find every direct property access. Each one needs either await (server) or use() (client).

Fix 6: cookies(), headers(), and draftMode() Are Also Async

The same migration applies to these helpers from next/headers:

// Old (Next 14):
import { cookies, headers } from "next/headers";

export default function Page() {
  const token = cookies().get("token")?.value;
  const ua = headers().get("user-agent");
  return ...;
}

// New (Next 15):
import { cookies, headers } from "next/headers";

export default async function Page() {
  const cookieStore = await cookies();
  const headerStore = await headers();
  const token = cookieStore.get("token")?.value;
  const ua = headerStore.get("user-agent");
  return ...;
}

Same with draftMode():

import { draftMode } from "next/headers";

export default async function Page() {
  const { isEnabled } = await draftMode();
  return isEnabled ? <DraftBanner /> : null;
}

cookies() returns a Promise-like object — you can await it once and then call .get, .set, .delete as before.

Fix 7: Middleware Params Stay Synchronous

Middleware runs at the edge and predates the async params change. request.nextUrl.searchParams is still synchronous in middleware.ts:

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const query = request.nextUrl.searchParams.get("q");
  // No await here — middleware uses the Web Request API directly.
  return NextResponse.next();
}

Same for request.cookies in middleware — that’s the edge runtime’s RequestCookies, not the App Router’s cookies().

Common Mistake: Trying to await request.nextUrl.searchParams in middleware. It’s already sync; awaiting a non-thenable returns the value unchanged but adds confusion.

Fix 8: Type Errors After Migration

After updating to async params, you’ll see TypeScript errors if you copied types wrong:

Type '{ slug: string; }' is not assignable to type 'Promise<{ slug: string; }>'.

Update every type annotation:

// Before:
type Props = { params: { slug: string } };

// After:
type Props = { params: Promise<{ slug: string }> };

A handy alias for project-wide consistency:

// types/next.ts
export type PageProps<P extends Record<string, string | string[]> = {}> = {
  params: Promise<P>;
  searchParams: Promise<Record<string, string | string[] | undefined>>;
};

Then:

import type { PageProps } from "@/types/next";

export default async function Page({ params }: PageProps<{ slug: string }>) {
  const { slug } = await params;
  ...
}

Still Not Working?

A few less-obvious failures:

  • Production-only failures. Sync access logs a warning in dev but throws in production. Test a next build && next start before deploying.
  • Sentry / OpenTelemetry instrumentation breaks. Older Sentry/OTel SDKs wrap headers()/cookies() synchronously. Upgrade to versions that handle the async API.
  • use(params) inside Server Components. Don’t. use is meant for Client Components or for unwrapping Promises in Server Components that can’t be async. If your component can be async, await is cleaner.
  • Catch-all routes ([...slug]). The type is Promise<{ slug: string[] }>, not Promise<{ slug: string }>. Awaiting gives you the array.
  • Optional catch-all ([[...slug]]). The type is Promise<{ slug?: string[] }> — slug can be undefined when the route matches the bare path.
  • searchParams is empty in dev but populated in prod. You’re reading it from a Client Component’s initial render, before hydration completes. Use useSearchParams() (reactive) instead of use(searchParams) (read-once).
  • Caching changed too. Next 15 made fetch uncached by default. If your data looks stale, that’s a separate issue — add cache: "force-cache" or use unstable_cache.
  • params is {} on a route with no segments. That’s correct for routes like app/page.tsx (no dynamic segments). You shouldn’t be reading params there at all.

For related Next.js App Router issues, see Next.js app router fetch cache, Next.js hydration failed, Next.js middleware not running, and Next.js server action not working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles