The Problem
I ran into this on a client e-commerce build during an upgrade from Next.js 15 to 16.2. Their shop listing page accepts ?page=2&sort=price-asc&color=blue and renders a paginated product grid. After the upgrade, every navigation that changed a query string threw the same error in the server logs:
TypeError: Cannot read properties of undefined (reading 'page')
at ShopPage (app/shop/page.tsx:14:34)
The same component compiled fine. The route resolved. The build passed. The moment the user hit the page with any query string, the Server Component crashed and the client got a 500. Stripping the query made the page render again, which made it look like a query-handling bug. It isn't. It is a typed API change between 15 and 16 that 16.2 now enforces strictly instead of warning.
If your app/**/page.tsx files read searchParams directly and you upgraded to 16.2, you will see this exact error the first time any page receives a query string.
Why It Happens
In Next.js 15, searchParams was a synchronous object. You destructured it inline: function Page({ searchParams }) { const { page } = searchParams }. Next.js 16 promoted that prop to a Promise to support partial prerendering, where the surrounding shell renders ahead of the dynamic query data. 16.0 and 16.1 logged a runtime warning if you read it synchronously. 16.2 stopped warning and started throwing.
The TypeError reads like a missing object access because that is exactly what happens. The prop is a Promise<SearchParams>. Destructuring it synchronously gives you undefined for every key, and the first downstream access crashes the render. The error is at line 14 because that is where you first read a key, not where the actual problem is.
Three confusing things make this hard to spot:
- The build does not catch it. TypeScript flags it only if your project's generated
.next/types/app/**/page.d.tsis fresh. After the upgrade, the old.d.tsfiles are still in.nextfrom the previous build and TypeScript typessearchParamsas the old shape until you delete.nextand rebuild. - Dev mode swallows it on the first request. The first request after
next devboots the route; the Promise resolves before the component renders because nothing else is racing it. The crash starts happening on the second navigation when streaming is active. - The same change applies to
paramsandcookies()andheaders(). If you only fixsearchParams, the next error appears in a layout that destructuresparamssynchronously, and the cycle starts again.
The Next.js 15 to 16 upgrade guide documents the async API contract.
The Fix
You need to await searchParams at the page boundary, propagate the resolved object to any child Server Components that need it, and run the codemod for the rest of the project so the same issue does not recur in 30 other files.
Step 1: Make the page component async and await the prop. If your page looked like this:
// app/shop/page.tsx (broken in 16.2)
export default function ShopPage({
searchParams,
}: {
searchParams: { page?: string; sort?: string; color?: string }
}) {
const page = Number(searchParams.page ?? 1)
return <ProductGrid page={page} sort={searchParams.sort} />
}
Change it to:
// app/shop/page.tsx (works in 16.2)
type SearchParams = { page?: string; sort?: string; color?: string }
export default async function ShopPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
const params = await searchParams
const page = Number(params.page ?? 1)
return <ProductGrid page={page} sort={params.sort} />
}
The component is now async, the prop is typed as Promise<SearchParams>, and the destructure happens after the await. The render still streams because await of a resolved Promise does not block partial prerendering. Next.js holds this segment dynamic and renders the shell ahead of it.
Step 2: Stop passing the unresolved Promise into children. If <ProductGrid> previously took searchParams as a prop, you were passing a Promise, not the resolved object. Update both sides:
// components/ProductGrid.tsx
type Props = { page: number; sort?: string }
export async function ProductGrid({ page, sort }: Props) {
const products = await getProducts({ page, sort })
return <Grid products={products} />
}
Pass primitives, not the Promise. Children should not have to know searchParams is async.
Step 3: Run the codemod. Next.js ships a codemod that converts the rest of your app in one pass:
npx @next/codemod@latest next-async-request-api .
This rewrites synchronous params, searchParams, cookies(), and headers() calls into their async equivalents. Inspect the diff before committing because the codemod inserts await on plain function components, which forces them to become async and changes their export signature. Anything that imported the type of those functions needs a follow-up edit.
Step 4: Wipe .next before validating. Stale generated types are the reason this looks fixed in some files and broken in others:
rm -rf .next && pnpm build && pnpm start
Then hit /shop?page=2&sort=price-asc and confirm the 500 is gone. If the error moves to a different file, the codemod missed a destructure pattern, usually one nested inside an if branch or a ternary.
The Lesson
Next.js 16 made the dynamic request APIs async; 16.2 made the contract a hard error. Treat searchParams, params, cookies(), and headers() as Promises, await them once at the page or layout boundary, and pass resolved primitives into children. Stop relying on the dev server's first-request behaviour to validate the change — production streams differently and exposes the bug immediately.
If your App Router upgrade is firing five different runtime errors across the route tree, that is the kind of work I do. See my services. For a closely related upgrade trap, see Next.js cookies async error in route handlers.