"params Should Be Awaited Before Using Its Properties": Tested on Next.js 16
Since Next.js 15, params and searchParams are Promises, and so are cookies(), headers() and draftMode(). Reading params.slug directly gives you "Route "/blog/[slug]" used `params.slug`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties." In Next.js 15 that was a warning and the old way still worked. In Next.js 16 the old way is gone, and the failure is quieter than the message suggests.
I tested it on Next.js 16.4.4: a page, generateMetadata and searchParams read the old way, a cookies() call the old way, then each fix, in next dev and next start.
What happens on Next.js 16
code next dev production HTML
params.slug in a page error in terminal, 200 slug=undefined, 200
params.slug in generateMetadata error in terminal, 200 <title>meta undefined
searchParams.q in a page error in terminal, 200 q=undefined, 200
cookies().get("theme") - 500: cookies(...).get is not a function
await params clean slug=shoes
use(params) in a client page clean slug=shoes
useParams() in a client page clean slug=shoesThe dangerous rows are the first three. next build passed, the pages returned 200, and nothing appeared in the browser. The only signal was an error in the next dev terminal. In production the page simply rendered undefined, including in the <title> tag, which is what Google would index. cookies() at least fails loudly with a 500.
Error: Route "/p-sync/[slug]" used `params.slug`. `params` is a Promise and must be
unwrapped with `await` or `React.use()` before accessing its properties.
Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis
at Page (app/p-sync/[slug]/page.tsx:3:52)
GET /p-sync/shoes 200 in 243msWhy TypeScript didn't catch it
I expected the build's type check to fail on this, the Next.js 14 way of typing a page:
export default function Page({ params }: { params: { slug: string } }) {
return <p>slug={params.slug}</p>;
}It didn't. next build passed and the page rendered an empty slug. The route types Next.js generates in .next/types/validator.ts declare the page props as { params: Promise<...> } & any, and the & any lets an old-style type through. If you type params yourself, the compiler trusts you.
The generated PageProps helper does catch it. It is global, so there's nothing to import:
export default function Page(props: PageProps<"/blog/[slug]">) {
return <p>slug={props.params.slug}</p>;
}
// tsc: Property 'slug' does not exist on type 'Promise<{ slug: string; }>'.Run next typegen (or next dev or next build once) so the route types exist, then use PageProps and LayoutProps instead of hand-written types.
Fix 1: await params in server components
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <p>slug={slug}</p>;
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return { title: slug };
}The page has to be async. Do the same in generateMetadata, generateViewport, layouts, and route handlers, whose second argument is { params: Promise<...> } too. searchParams works the same way: const { q } = await searchParams.
Fix 2: use(params) in client pages
"use client";
import { use } from "react";
export default function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = use(params);
return <p>slug={slug}</p>;
}Client components can't be async, so unwrap the Promise with React's use().
Fix 3: useParams() in nested client components
"use client";
import { useParams } from "next/navigation";
export function Breadcrumb() {
const { slug } = useParams<{ slug: string }>();
return <span>{slug}</span>;
}useParams() is a hook and was never a Promise, so it's unaffected. It's the easiest option deep inside a client component tree, instead of passing params down.
cookies() and headers()
These are Promises now too: const theme = (await cookies()).get("theme")?.value. Called the old way, cookies().get threw "get is not a function" and the page returned a 500 in production.
The codemod
npx @next/codemod@latest next-async-request-api .I ran it (version 16.4.5). It rewrote my page.tsx to an async function that awaits props.params, with the type changed to a Promise. Two catches. It only changed the file when it sat in an app/ route folder: the same code in a loose file was left alone. And for the cookies() call it didn't fix anything: it added a cast to UnsafeUnwrappedCookies and a @next-codemod-error comment telling you to await it yourself. Search for @next-codemod-error after running it, and type-check with PageProps to find what it missed.
Checklist
- Search the code for params. and searchParams. used without await or use().
- Watch the next dev terminal: this error never shows in the browser.
- Type pages with PageProps<"/route"> and layouts with LayoutProps, not hand-written { params: { slug: string } }.
- Check generateMetadata: a sync read there puts "undefined" in your title.
- After the codemod, fix every @next-codemod-error comment.
Related
- useSearchParams() should be wrapped in a Suspense boundary
- generateMetadata and the current URL
- Next.js hydration error: 6 causes tested
- Next.js development