useSearchParams Suspense Error: 3 Real Fixes (Next.js 15)
The error only surfaces during the production build — dev mode hides it completely. Here is why, and the three fixes ranked by situation.
A search UI can work in next dev and then fail during the production build with:
Error: Missing Suspense boundary with useSearchParams
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailoutThe confusing part: this error never appears in development. In dev, routes render on-demand, so useSearchParams never suspends. During next build, static pages are prerendered ahead of time — and that is when Next.js hits the wall.
Why this happens#
useSearchParams is a Client Component hook that reads URLSearchParams from the current URL. The problem is that URL query strings are request-time data — they cannot be known at prerender time. When Next.js tries to statically render a page that contains useSearchParams (directly or in a child component), it needs a Suspense boundary to split the page: the static shell can be prerendered, and the dynamic part with useSearchParams gets hydrated client-side.
Without that boundary, Next.js has nowhere to cut — it fails the build rather than silently serving wrong content.
The rule from the Next.js docs (v16.2.9): during production builds, a static page that calls useSearchParams from a Client Component must be wrapped in a Suspense boundary, otherwise the build fails.
Fix 1 — Wrap in Suspense (fastest)#
This is the minimal fix. Wrap the component using useSearchParams in a <Suspense> boundary with a fallback:
import { Suspense } from 'react'
import SearchBar from './search-bar'
function SearchBarFallback() {
return <div className="h-10 w-full animate-pulse bg-gray-100 rounded" />
}
export default function Page() {
return (
<>
<nav>
<Suspense fallback={<SearchBarFallback />}>
<SearchBar />
</Suspense>
</nav>
<h1>Dashboard</h1>
</>
)
}'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
return <input defaultValue={search ?? ''} placeholder="Search..." />
}The fallback renders in the prerendered HTML. Once the page hydrates, React replaces it with the real SearchBar.
When to use this: when the component genuinely needs to live in a Client Component and you want to keep the logic self-contained.
Fix 2 — Use the searchParams page prop (recommended for Server Components)#
If the parent Page component is a Server Component (which is the default), you can read search params directly from the searchParams prop and pass the value down as a plain prop. No hook, no Suspense required:
import SearchBar from './search-bar'
export default async function Page({ searchParams }) {
const { search } = await searchParams
return (
<>
<nav>
<SearchBar initialSearch={search ?? ''} />
</nav>
<h1>Dashboard</h1>
</>
)
}'use client'
export default function SearchBar({ initialSearch }) {
return <input defaultValue={initialSearch} placeholder="Search..." />
}One important caveat: Layouts do NOT receive the searchParams prop. Only Pages do. If you need search params inside a Layout, you must use useSearchParams with a Suspense boundary.
Reading the page's searchParams prop opts the page into dynamic rendering, because its value depends on the request. When to use this: when the Page already coordinates request-time data. To retain a static shell, use Fix 1 instead.
Fix 3 — Force dynamic rendering with connection()#
If the page genuinely should be dynamic (never prerendered), use connection() from next/server in the parent Server Component. This tells Next.js to wait for an actual incoming request before rendering, which makes useSearchParams safe without Suspense:
import { connection } from 'next/server'
import SearchBar from './search-bar'
export default async function Page() {
await connection()
return (
<>
<nav>
<SearchBar />
</nav>
<h1>Dashboard</h1>
</>
)
}The Next.js docs prefer connection() to express waiting for an incoming request. In Next.js 15, export const dynamic = 'force-dynamic' remains a documented route segment option; this preference is not a deprecation notice.
When to use this: when the entire page is inherently dynamic (authenticated dashboards, personalized feeds). You are trading prerender performance for simplicity.
Verify the fix by reading the route table#
After the build passes, check that the route's rendering mode matches the chosen remedy. Run a clean production build and check the marker next build prints next to your route:
rm -rf .next
npx next buildRoute (app)
┌ ○ /_not-found
├ ○ /
└ ƒ /dashboard
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand○ means the route was prerendered into static HTML at build time — that's the expected outcome for Fix 1 when no other request-time API makes the route dynamic. ƒ means the route is rendered on demand per request. It is expected for both Fix 2 (reading the page's searchParams prop) and Fix 3 (connection()), as well as an explicit force-dynamic setting. A dynamic marker after Fix 1 calls for checking other request-time data, such as cookies() or headers(); the marker alone does not mean the Suspense boundary failed.
One placement mistake to watch for while you're in there: the <Suspense> boundary has to be an ancestor of the component calling useSearchParams, not a sibling or a child. If your tree is <Page> (itself calling the hook) → <Suspense> → <Child>, the hook runs before the boundary is reached and you still get the build error — wrapping the wrong node in <Suspense> doesn't rescue a hook call that happens above it.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
When the fix is the wrong fix#
Do not put connection() on a marketing page or a blog post to silence this error. Forcing dynamic rendering means the page is server-rendered on every request — you lose the static prerender performance that made you choose App Router in the first place.
If useSearchParams appears in a component buried inside a page that is otherwise fully static, the right answer is Fix 1 (Suspense) — not Fix 3. The Suspense boundary lets the static parts prerender while only the search bar is hydrated.
Reserve space for the search bar while it loads. A null Suspense fallback is valid, but can cause a layout shift when a visible control replaces an empty area. A skeleton or a container with the final dimensions avoids that specific shift.
Checklist after applying the fix#
- Run
next buildlocally and confirm it succeeds (do not rely onnext dev). - Verify the
Suspensefallback matches the height and width of the real component to avoid CLS. - If using Fix 2 (searchParams prop), confirm the component using the prop does NOT also call
useSearchParamsinternally. - If using Fix 3 (connection), confirm the page is meant to be dynamic, not static.
- Deploy and check the production page: the search bar should render correctly on first load without a flash.
Static export removes one of the three fixes entirely#
Fix 2 and Fix 3 require request-time server rendering. For output: 'export',
use Fix 1. A project configured with output: 'export' uses a
fully static export with no Next.js server at runtime, the mode used to deploy to plain
static hosting — Fix 3 (connection(), or the older export const dynamic = 'force-dynamic') is not an option. Both mechanisms work by deferring rendering until an
actual incoming request arrives at a server; a static export has no server process at
request time to defer to, only pre-generated HTML files. Using either in a static export
build fails for a different reason than the "missing Suspense boundary" error covered
above: there is no request to wait for.
That leaves Fix 1 (wrap the component using useSearchParams in <Suspense>) as the
only viable path for a page that needs both static export and a component that reads the
URL's query string. The static shell prerenders at build time, the Suspense fallback
ships in the static HTML, and the real value only becomes available client-side once the
page hydrates in the browser and reads window.location — which is also why a page that
depends on searchParams for its initial content (rather than a client-only
enhancement) is a poor fit for static export in the first place: the server-rendered
searchParams page prop from Fix 2 requires a Server Component render per request, which
static export cannot do at all.
If the project mixes static export with Partial Prerendering concepts — prerendering a
static shell while deferring specific dynamic parts — it's worth understanding the
difference in mechanism before choosing between them:
Next.js 15 Partial Prerendering: Guide
covers how PPR uses Suspense boundaries to mark the dynamic "holes" in an otherwise
static page on a server-rendered deployment, which is a related but distinct feature from
output: 'export' — PPR still runs a Next.js server at request time to fill those holes,
while static export runs no server at all. Confusing the two leads to exactly this
symptom: a fix that works in one deployment mode silently doesn't apply in the other.
Further reading: Next.js Scroll Resets When searchParams Change, use client vs use server in Next.js: The Real Difference.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix localStorage is not defined after 'use client' (Next.js)
The cookie banner from the Stack Overflow question, rebuilt on Next.js 16.3.8: why 'use client' does not stop the crash, the useEffect fix, and the other errors Node 25 and 26 print.
use client vs use server in Next.js: The Real Difference
The #1 misconception in the App Router: "use server" is not the opposite of "use client". One marks a client boundary in the module graph; the other exposes callable server functions. Server Components are the default — no directive needed.
Fix: react-dom/server Import Error in Next.js App Router
"You're importing a component that imports react-dom/server" is a compile-time rule of the App Router, not a bug in your code. Here is what the rule protects, which use cases are legitimate, and the fixes verified against Next.js 15.5 and 16.3 with both webpack and Turbopack.


