Overview

In Next.js 16, params, searchParams, cookies(), headers(), and draftMode() can be read only asynchronously. Next.js 15 introduced the Promise form with a temporary synchronous fallback; Next.js 16 removed the fallback, so the Next.js 14 pattern (params.slug, cookies().get(...)) now fails. Async access lets Next.js start rendering before request data resolves. The routing conventions are in nextjs-app-router; the effect on prerendering is in nextjs-caching.

Await params and searchParams everywhere they appear

Pages, layouts, route handlers, default.js, and generateMetadata all receive Promises. Type them with the generated helpers so the Promise shape stays correct.

// app/posts/[slug]/page.tsx
export default async function Post(props: PageProps<"/posts/[slug]">) {
  const { slug } = await props.params;
  const { ref } = await props.searchParams;
  return <Article post={await getPost(slug)} referrer={ref} />;
}

next typegen generates PageProps, LayoutProps, and RouteContext. In a route handler, use ctx: RouteContext<"/users/[id]"> and await ctx.params. A page and its generateMetadata each await the same Promise independently.

Await cookies(), headers(), and draftMode()

import { cookies, headers, draftMode } from "next/headers";
 
const session = (await cookies()).get("session")?.value;
const locale = (await headers()).get("accept-language");
const { isEnabled } = await draftMode();

Handle image routes and sitemaps

opengraph-image, twitter-image, icon, and apple-icon now receive params as a Promise, and the id from generateImageMetadata as Promise<string>; generateImageMetadata itself still gets synchronous params. A sitemap function receives its id from generateSitemaps as a Promise<string>; convert with Number(await id).

Run the codemod, then fix what it misses

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

The upgrade codemod does not run this migration for you, so run it explicitly. It awaits the APIs and updates prop types, but it cannot infer intent in helpers that pass params through several layers or in client components. After it runs, search for leftover synchronous reads and for call sites it flagged with comments.

Resolve once at the route entry

Await request data at the top of the route and pass plain values inward, so helpers stay synchronous and the async surface stays at the edge. Client Components cannot await: read the values in a Server Component and pass them down, or unwrap the Promise with use(params).

Await inside <Suspense> when Cache Components is on

With cacheComponents enabled, an await params or await cookies() at the top of a layout makes the whole subtree request-bound, so no static shell is produced. Pass the Promise down and await it in a child under <Suspense>.

export default function Layout({ children, params }: LayoutProps<"/shop/[slug]">) {
  return (
    <>
      <Sidebar />
      <Suspense fallback={<h1>Loading...</h1>}>
        {params.then(({ slug }) => <SlugHeading slug={slug} />)}
      </Suspense>
      {children}
    </>
  );
}

Params returned by generateStaticParams are known at build time and prerender normally.