Skip to content

NEXT.JS

Next.js 15 Async Request APIs: Migrating params and searchParams Promises

Next.js 15 made params and searchParams asynchronous Promises. Accessing properties synchronously throws the sync-dynamic-apis error. Here is the contract across Server Components, Client Components, and Route Handlers.

In Next.js 15, dynamic route properties (params and searchParams) are asynchronous Promises. Accessing params.slug directly without await throws the sync-dynamic-apis runtime error. This architectural shift enables concurrent streaming and optimistic static prerendering, but breaks synchronous patterns from Next.js 13 and 14. Here is the verified contract across Server Components, generateMetadata, Client Components via React 19 use(), and Route Handlers.

Key takeaways

  • params and searchParams are typed as Promise<{ [key: string]: string }> across all App Router dynamic routes.
  • Accessing properties synchronously triggers the Next.js 15 sync-dynamic-apis build error.
  • Server Components and generateMetadata must explicitly await params before destructuring.
  • Client Components unwrap Promise props natively with React 19's use(params) hook without useEffect or state plumbing.
  • cookies() and headers() from next/headers are also asynchronous and require await.

The Next.js 15 breaking change: Asynchronous request APIs

In Next.js 14, page props arrived as plain JavaScript objects. A dynamic article route accessed parameters synchronously:

Legacy Next.js 14 (Deprecated)
// BREAKS IN NEXT.JS 15: params is no longer a plain object
export default function LegacyPage({
  params,
}: {
  params: { category: string; slug: string };
}) {
  return <h1>{params.slug}</h1>;
}

Per the Next.js 15 Upgrade Guide, dynamic request-specific data was transitioned to Promises. In earlier versions, reading synchronous request APIs forced Next.js to wait for incoming HTTP headers to be fully ingested before executing any component rendering.

By turning params, searchParams, cookies(), and headers() into Promises, the App Router server can immediately stream initial HTML shells and static layouts, deferring parameter resolution until the specific component that consumes them awaits the promise.

The sync-dynamic-apis error and why it triggers

When legacy code reads a property on params without awaiting it, Next.js 15 intercepts the property access via a JavaScript Proxy and prints the following error in development and production builds:

text
Error: Route "/[category]/[slug]" used "params.slug".
`params` should be awaited before using its properties.
Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis

This error halts the build process. A common mistake during migrations is applying an un-awaited (await params).slug inline inside JSX expressions or forgetting that generateMetadata receives the exact same Promise shape.

Our internal audit script scans all route files in app/ using static AST checks to ensure zero un-awaited synchronous parameter accesses exist across the codebase.

Unwrapping params in Server Components and generateMetadata

In Server Components, resolving params requires updating the interface type definition and adding a single await statement at the top of the function body.

Here is the exact implementation used on this site's dynamic article route:

app/[category]/[slug]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getArticle, getArticleParams } from "@/lib/mdx";

type PageProps = {
  params: Promise<{ category: string; slug: string }>;
};

/** Every published article is prerendered; unknown routes 404. */
export async function generateStaticParams() {
  return getArticleParams();
}

export const dynamicParams = false;

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  // Await the params Promise before reading category or slug
  const { category, slug } = await params;
  const article = getArticle(category, slug);
  if (!article) return {};

  return {
    title: article.meta.title,
    description: article.meta.deck,
  };
}

export default async function ArticlePage({ params }: PageProps) {
  // Await the params Promise in the main component body
  const { category, slug } = await params;
  const article = getArticle(category, slug);
  if (!article) notFound();

  return (
    <article>
      <h1>{article.meta.title}</h1>
      <div>{article.content}</div>
    </article>
  );
}

Notice that generateMetadata and ArticlePage share the identical PageProps definition. Treating params as a Promise in the page component while leaving generateMetadata typed synchronously will trigger the sync-dynamic-apis failure during metadata extraction.

Unwrapping params in Client Components with React 19 use

If a dynamic route segment is marked with "use client", it cannot be declared as an async function. In older React versions, unwrapping a Promise in a Client Component required useState, useEffect, and loading boilerplate.

In Next.js 15 with React 19, Client Components unwrap the incoming Promise cleanly using the native use() hook:

app/tag/[tag]/TagClientView.tsx
"use client";

import { use } from "react";

type ClientPageProps = {
  params: Promise<{ tag: string }>;
};

export default function TagClientView({ params }: ClientPageProps) {
  // React 19 use() hook suspends and unwraps the Promise synchronously
  const { tag } = use(params);

  return (
    <div>
      <h2>Tagged with: {tag}</h2>
    </div>
  );
}

When React encounters use(params), it integrates directly with React Suspense boundaries. If the Promise is pending, React suspends rendering until the Promise resolves, then continues execution with the resolved object.

Route handlers and layout inheritance

The asynchronous change also applies to API Route Handlers (route.ts) and dynamic Layouts (layout.tsx).

Route Handlers

Route handlers receive params as the second argument inside the context object. The context object itself is synchronous, but its params property is an asynchronous Promise:

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

export async function GET(
  request: Request,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params;

  return NextResponse.json({
    status: "ok",
    articleSlug: slug,
  });
}

Dynamic Layouts

Dynamic layout files (app/[category]/layout.tsx) receive params: Promise<{ category: string }> as a prop alongside children: React.ReactNode. If your layout renders breadcrumbs, category headers, or navigation badges based on the active route segment, remember to await params in the layout function:

app/[category]/layout.tsx
type LayoutProps = {
  children: React.ReactNode;
  params: Promise<{ category: string }>;
};

export default async function CategoryLayout({
  children,
  params,
}: LayoutProps) {
  const { category } = await params;

  return (
    <section>
      <header className="border-b border-line pb-4">
        <span className="font-mono text-accent uppercase">{category}</span>
      </header>
      {children}
    </section>
  );
}

Best practices

  • Centralize PageProps types: Define a shared PageProps type for your route and reuse it across both generateMetadata and the default page export.
  • Pair with generateStaticParams: For content-driven blogs, combine asynchronous params with export const dynamicParams = false to guarantee static compilation during builds.
  • Await cookies() and headers() early: Call const cookieStore = await cookies() at the top of Server Actions and route handlers before reading values.
  • Enforce automated CI guards: Use static analysis (like our internal audit script) to verify that no direct un-awaited parameter reads slip into production.

Common mistakes

  • Destructuring in function signatures: Writing function Page({ params: { slug } }) immediately accesses the property on the unresolved Promise and throws sync-dynamic-apis.
  • Forgetting generateMetadata: Updating the component body to await params while leaving generateMetadata with synchronous destructuring causes silent metadata build failures.
  • Adding async to Client Components: Declaring "use client" on an async component is invalid in React. Use use(params) instead.
  • Assuming searchParams is static: Unlike params (which can be statically generated with generateStaticParams), searchParams opt routes into dynamic rendering on demand.

Conclusion

Migrating to Next.js 15's asynchronous request APIs requires a mental shift from synchronous prop inspection to asynchronous Promise unwrapping. By updating your route contracts to declare params and searchParams as Promises, awaiting them in Server Components and generateMetadata, and utilizing React 19's use() hook in Client Components, you eliminate sync-dynamic-apis build errors completely.

Review our guides on caching database queries with unstable_cache and diagnosing Next.js 500 build errors for debugging other common Next.js 15 compilation failure modes.

Frequently asked questions

Why did Next.js 15 make params and searchParams asynchronous?
Synchronous dynamic APIs forced the Next.js server to block component rendering until incoming request headers and URL parameters were fully parsed. By converting params, searchParams, cookies(), and headers() into Promises, Next.js 15 can initiate server rendering, stream static HTML shells immediately, and unwrap dynamic request parameters concurrently when requested.
How do you fix 'Error: Route used params.slug. params should be awaited'?
Update your TypeScript PageProps type definition to type params as Promise<{ slug: string }>, and await the promise inside your component body (const { slug } = await params;). If you are using generateMetadata, apply the identical PageProps type and await params before constructing metadata objects.
How do Client Components receive params in Next.js 15?
When a dynamic route segment is a Client Component ('use client'), Next.js passes params as a Promise. Use React 19's native use() hook (const { slug } = use(params);) to unwrap the Promise directly inside the Client Component without adding useEffect or local loading state.
Are cookies() and headers() also asynchronous in Next.js 15?
Yes. In Next.js 14 and earlier, cookies() and headers() were synchronous helper functions. In Next.js 15, both functions return Promises and must be awaited (const cookieStore = await cookies(); const headerStore = await headers();). Calling synchronous cookie methods without await triggers runtime deprecation warnings and build errors.

Muhammad Kashif

Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.