Fix: Next.js 'params should be awaited before using its properties'
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
awaitparams. They run in an async context, so this is straightforward. - Client Components can’t
awaitat the top level, so they useReact.use(params)instead.React.useis 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:
- The component is
async. - The
paramstype isPromise<{ slug: string }>, not{ slug: string }. - 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
asyncto Server Components that read params or searchParams. - Inserts
await paramsbefore property access. - Wraps Client Component reads with
React.use()and adds theimport. - 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 startbefore 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.useis meant for Client Components or for unwrapping Promises in Server Components that can’t beasync. If your component can beasync,awaitis cleaner.- Catch-all routes (
[...slug]). The type isPromise<{ slug: string[] }>, notPromise<{ slug: string }>. Awaiting gives you the array. - Optional catch-all (
[[...slug]]). The type isPromise<{ slug?: string[] }>— slug can be undefined when the route matches the bare path. searchParamsis empty in dev but populated in prod. You’re reading it from a Client Component’s initial render, before hydration completes. UseuseSearchParams()(reactive) instead ofuse(searchParams)(read-once).- Caching changed too. Next 15 made
fetchuncached by default. If your data looks stale, that’s a separate issue — addcache: "force-cache"or useunstable_cache. paramsis{}on a route with no segments. That’s correct for routes likeapp/page.tsx(no dynamic segments). You shouldn’t be readingparamsthere 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React 19 Actions Not Working — useActionState, useFormStatus, useOptimistic, and form action
How to fix React 19 actions errors — useActionState signature, form action vs onSubmit, useFormStatus must be in child, useOptimistic state desync, Server Actions in client components, and error handling.
Fix: SWR Not Working — Key Changes, Mutate Not Updating, Conditional Fetching, and SSR Hydration
How to fix SWR errors — useSWR not refetching on key change, mutate not invalidating, conditional null key, fallbackData vs fallback, SSR hydration mismatch, infinite scroll pagination, and TypeScript typing.
Fix: Clerk Not Working — Auth Not Loading, Middleware Blocking, or User Data Missing
How to fix Clerk authentication issues — ClerkProvider setup, middleware configuration, useUser and useAuth hooks, server-side auth, webhook handling, and organization features.
Fix: next-safe-action Not Working — Action Not Executing, Validation Errors Missing, or Type Errors
How to fix next-safe-action issues — action client setup, Zod schema validation, useAction and useOptimisticAction hooks, middleware, error handling, and authorization patterns.