Stop Next.js 16 Crashes: Fixing `searchParams` Errors

Learn to normalize `searchParams` in Next.js 15 and avoid runtime crashes with our step-by-step guide.

·Matija Žiberna·
Stop Next.js 16 Crashes: Fixing `searchParams` Errors

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

I was working on a Next.js 16 project, happily paging through admin lists, when Vercel started screaming: “TypeError: Cannot read properties of undefined (reading '0')” right inside .next/server/app/(admin)/admin/dobavnice/page.js. React 19 and Turbopack already switched searchParams from the old plain object into a ReadonlyURLSearchParams, so every params.foo?.[0] in my server components fell apart at runtime. This guide shows how to normalize the new shape once, reuse it everywhere, and stop the cold-start crashes on Vercel.

Step 1: Reproduce the failure and understand the new shape

Hit any page that still does Array.isArray(params.foo) on Next.js 15 and you’ll see the same stack trace:

TypeError: Cannot read properties of undefined (reading '0')
    at async m (.next/server/app/(admin)/admin/dobavnice/page.js:1:9228)

Server components now receive a promise that resolves to a ReadonlyURLSearchParams. Missing keys return undefined, so params.page[0] blows up. The fix is to read search params through a helper that understands every shape Next has ever returned: plain records, URLSearchParams, or nothing at all.

Step 2: Add a helper that normalizes searchParams

Create or update src/lib/utils.ts with a helper that unwraps the key cleanly.

// File: src/lib/utils.ts
import type { ReadonlyURLSearchParams } from "next/navigation"
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export type SearchParamsLike =
  | ReadonlyURLSearchParams
  | URLSearchParams
  | Record<string, string | string[] | undefined>
  | null
  | undefined

export const getSearchParamValues = (
  params: SearchParamsLike,
  key: string
): string[] => {
  if (!params) return []

  if (typeof (params as URLSearchParams).getAll === "function") {
    const values = (params as URLSearchParams).getAll(key)
    return values.filter((value): value is string => typeof value === "string")
  }

  const record = params as Record<string, string | string[] | undefined>
  const raw = record?.[key]

  if (Array.isArray(raw)) {
    return raw.filter((value): value is string => typeof value === "string")
  }

  return typeof raw === "string" ? [raw] : []
}

export const getSearchParamValue = (
  params: SearchParamsLike,
  key: string
): string | undefined => {
  const [value] = getSearchParamValues(params, key)
  return value
}

The helper checks for URLSearchParams compatibility (getAll), falls back to the legacy record shape, and returns an empty array for nullish input. getSearchParamValue mirrors the old “first element or nothing” behaviour without the brittle [0] access.

Step 3: Update server components to use the helper

Replace the manual Array.isArray logic in affected pages. Example refactor:

// File: src/app/(admin)/admin/dobavnice/page.tsx
type AdminDeliveryNotesListPageProps = Readonly<{
  searchParams: Promise<SearchParamsLike>;
}>;

export default async function AdminDeliveryNotesListPage({
  searchParams,
}: AdminDeliveryNotesListPageProps) {
  const params = await searchParams;

  const page = Number.parseInt(getSearchParamValue(params, "page") ?? "1", 10);
  const search = getSearchParamValue(params, "search") ?? "";
  const statusParam = getSearchParamValue(params, "status");
  const status = statusParam ? (statusParam as DeliveryNoteStatus) : undefined;
  // ...
}

The rendering stays identical. Swap in the helper, update the prop type to Promise<SearchParamsLike>, and keep the business logic untouched. Repeat for other pages that previously relied on Array.isArray(params.foo).

Step 4: Redeploy and keep your build green

Run pnpm build (or NEXT_DISABLE_TURBOPACK=1 pnpm next build) locally to make sure the helper compiles. Redeploy to Vercel and the TypeError disappears because the runtime no longer dereferences [0] on undefined. Pagination and filters work exactly as before, only now they’re resilient to future Next.js changes.

Wrap-up

Next.js 15 and React 19 changed the shape of searchParams, and the helper is the difference between healthy admin lists and Vercel’s “Cannot read properties of undefined” panic. By centralising the normalization and reusing it across your server components, you keep pagination, filters, and driver dashboards safe from the breaking change—all without rewriting your business logic.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija

3

Comments

Leave a Comment

Your email will not be published

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.