- name
- troubleshoot
- description
- Diagnose Next.js 16 errors — hydration mismatches, 'use client' and server-only errors, build failures such as dynamic server usage, Cache Components errors like blocking-prerender and next-request-in-use-cache, next/image host errors, slow dev or build, and deployment problems. Use when something fails or behaves unexpectedly in a Next.js app.
# Troubleshooting Next.js
Diagnostics and fixes for common Next.js issues. Start with the quick diagnostics checklist, then check the categorized error tables.
## Quick Diagnostics Checklist
1. **Check Next.js version**: `npx next --version` — ensure 14+ for stable App Router; on 16.3 be at `16.3.8` or later (security fixes)
2. **Check Node.js version**: `node -v` — `>=20.9.0` is the formal minimum for Next.js 16, but use Node 22 or 24 LTS (Node 20 is end-of-life)
3. **Check the caching model**: is `cacheComponents: true` in `next.config.ts`? It decides which fixes below apply — `export const dynamic` / `revalidate` / `dynamicParams` work only when it is off
4. **Clear cache**: `rm -rf .next && npm run dev`
5. **Check TypeScript**: `npx tsc --noEmit` — catch type errors early
6. **Check console**: Both browser console and terminal for error messages
7. **Check MCP**: If using `next-devtools-mcp`, call `nextjs_call` with `get_errors`
## Hydration Errors
The most common Next.js issue. Occurs when server-rendered HTML doesn't match client-rendered output. Since Next.js 16.2, the dev overlay shows a labeled `+Client`/`-Server` diff pinpointing the exact mismatching markup.
| Error | Cause | Fix |
|-------|-------|-----|
| "Text content does not match" | Different text on server vs client | Avoid `Date.now()`, `Math.random()` in Server Components. Use `useEffect` for client-only values |
| "Expected server HTML to contain a matching `<div>`" | DOM structure mismatch | Check for `<p>` inside `<p>`, `<div>` inside `<p>`, or conditional rendering based on `window` |
| "Hydration failed because the initial UI does not match" | Browser extensions or invalid HTML nesting | Validate HTML nesting. Suppress specific elements with `suppressHydrationWarning` |
### Common Hydration Causes
```tsx
// BAD: Different output on server vs client
export default function Clock() {
return <p>Time: {new Date().toLocaleTimeString()}</p>
}
// GOOD: Use client-only rendering for dynamic values
'use client'
import { useState, useEffect } from 'react'
export default function Clock() {
const [time, setTime] = useState<string>()
useEffect(() => {
setTime(new Date().toLocaleTimeString())
}, [])
return <p>Time: {time ?? 'Loading...'}</p>
}
```
```tsx
// BAD: Checking window in render
export default function Component() {
const isDesktop = window.innerWidth > 768 // Fails on server
return isDesktop ? <Desktop /> : <Mobile />
}
// GOOD: Use CSS media queries or useEffect
'use client'
import { useState, useEffect } from 'react'
export default function Component() {
const [isDesktop, setIsDesktop] = useState(false)
useEffect(() => {
setIsDesktop(window.innerWidth > 768)
}, [])
return isDesktop ? <Desktop /> : <Mobile />
}
```
### Locale-Dependent Date Rendering
`toLocaleDateString()` produces different output on server vs client because locale settings differ. Three approaches, from best to simplest:
**Approach 1 — ISO initial render + client upgrade (recommended):**
Render a stable ISO string on the server, then upgrade to the user's locale format after hydration. No mismatch, no flash of wrong content for most users:
```tsx
// components/formatted-date.tsx
'use client'
import { useState, useEffect } from 'react'
export function FormattedDate({ dateString }: { dateString: string }) {
const [formatted, setFormatted] = useState(() => {
// Stable initial render — matches server output
return new Date(dateString).toISOString().split('T')[0]
})
useEffect(() => {
// After hydration, upgrade to user's locale format
setFormatted(new Date(dateString).toLocaleDateString())
}, [dateString])
return <time dateTime={dateString}>{formatted}</time>
}
```
Use in a Server Component page:
```tsx
// app/blog/[id]/page.tsx — Server Component
import { FormattedDate } from '@/components/formatted-date'
export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await getPost(id)
return (
<article>
<h1>{post.title}</h1>
<FormattedDate dateString={post.date} />
<p>{post.content}</p>
</article>
)
}
```
**Approach 2 — Fixed locale (simpler but less flexible):**
Pin the locale so server and client produce identical output:
```tsx
'use client'
export function FormattedDate({ dateString }: { dateString: string }) {
return (
<time dateTime={dateString}>
{new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric',
})}
</time>
)
}
```
**Approach 3 — `suppressHydrationWarning` (last resort):**
```tsx
<time dateTime={post.date} suppressHydrationWarning>
{new Date(post.date).toLocaleDateString()}
</time>
```
Only use `suppressHydrationWarning` for leaf elements where the mismatch is cosmetic (e.g., date formatting). It does not fix the mismatch — it silences the warning and shows the client output after hydration.
## `'use client'` Errors
| Error | Cause | Fix |
|-------|-------|-----|
| "useState is not a function" | Using hooks in a Server Component | Add `'use client'` to the file |
| "You're importing a component that needs useState" | Importing a client library in a Server Component | Create a wrapper Client Component |
| "Event handlers cannot be passed to Client Component props from Server Components" | Passing `onClick` from Server to Client Component | Define event handlers inside the Client Component |
| "Functions cannot be passed directly to Client Components" | Trying to pass a function as props | Move the function into the Client Component or use Server Actions |
## Build Errors
| Error | Cause | Fix |
|-------|-------|-----|
| "Dynamic server usage" | Using `cookies()`, `headers()`, or `searchParams` in a statically generated page | Without `cacheComponents`: add `export const dynamic = 'force-dynamic'` or restructure to fetch dynamically. With `cacheComponents: true` that export is removed — wrap the component that reads the runtime API in `<Suspense>` (see "Cache Components Errors" below) |
| "generateStaticParams is required for dynamic routes with output: export" | Missing static params for static export | Add `generateStaticParams` or remove `output: 'export'` |
| "Module not found: Can't resolve 'fs'" | Using Node.js modules in client code | Move to Server Component or Route Handler |
| "`params` is now a Promise" | Next.js 15+ async params not awaited | `const { id } = await params` |
## Cache Components Errors
Apply only when `cacheComponents: true`. Here every dynamic read is a choice: **stream** it behind `<Suspense>`, **cache** it with `'use cache'`, or (for uncached IO) block the route with `export const instant = false`. Do not reach for `export const dynamic = 'force-dynamic'` — it is removed under Cache Components (everything is dynamic by default). Next.js 16.3 prints these as labelled fix menus in the dev overlay, the `next dev` terminal and `next build`.
| Error / symptom | Cause | Fix |
|-----------------|-------|-----|
| "Next.js encountered uncached data during prerendering or a navigation" (Blocking Route, `blocking-prerender-dynamic`) | An uncached `fetch()`, database call or `await connection()` ran outside `<Suspense>` | Wrap the reader in `<Suspense fallback={...}>`, or cache it with `'use cache'` + `cacheLife()` (not applicable to `connection()`). Keep the boundary close to the data access — one around the whole page leaves an empty static shell |
| "Next.js encountered runtime data during prerendering or a navigation" (`blocking-prerender-runtime`) | `cookies()`, `headers()`, `params` or `searchParams` read outside `<Suspense>` | Pass the promise down and `await` it inside a `<Suspense>` boundary; with Cache Components this replaces `export const dynamic = 'force-dynamic'` |
| Build error on `new Date()`, `Date.now()`, `Math.random()`, `crypto.randomUUID()` during prerender | Synchronous non-deterministic read cannot be silently included in the static shell | Call `await io()` (from `next/cache`) before it, inside `<Suspense>`; or capture it once with `'use cache'` |
| `Route segment config "dynamicParams" is not compatible with nextConfig.cacheComponents` | `export const dynamicParams` is not supported with Cache Components | Delete the export; call `notFound()` when the param does not resolve to real data |
| `empty-generate-static-params` | `generateStaticParams` returned `[]` | Return at least one real param; other params still get a static shell and stream at request time |
| `next-request-in-use-cache` | `cookies()`, `headers()` or `searchParams` read inside a `'use cache'` scope | Read it outside and pass the value as an argument, or use `'use cache: private'` |
| Route still returns `200` but navigation blocks | Insights appear in the dev overlay, terminal and MCP `get_errors`, not in the HTTP response | Read the overlay or call `get_errors` through `next-devtools-mcp` |
## Data Fetching Errors
| Error | Cause | Fix |
|-------|-------|-----|
| "async/await is not yet supported in Client Components" | Using `async` in a Client Component | Fetch in a Server Component and pass data as props, or use `use()` hook |
| "cookies was called outside a request scope" | Accessing `cookies()` at build time | Ensure it's inside a request handler (page, layout, route handler, or Server Action) |
| "Error: NEXT_REDIRECT" | `redirect()` caught in try/catch | Call `redirect()` outside try/catch blocks — it throws intentionally |
| Stale data after mutation | Cache not revalidated | Call `revalidatePath()` or `revalidateTag()` after mutations |
## Image Issues (`next/image`)
| Symptom | Cause | Fix |
|---------|-------|-----|
| Broken/missing images, no visible error | Upstream image source returns 403 — `next/image` proxies through `/_next/image` so the HTTP error is hidden from the browser | Check network tab for `/_next/image` requests returning 403/401. If the upstream (e.g., Directus, S3) requires auth, make the files public in the upstream or serve them through a Route Handler of your own that adds the credential server-side. Never put a token or API key in the image URL: `/_next/image?url=` carries the full source URL into the HTML for every visitor |
| "Invalid src prop" or "hostname not configured" | Remote image domain missing from config | Add the domain to `images.remotePatterns` in `next.config.ts` |
| Images load in `<img>` but not `<Image>` | `next/image` optimizer can't fetch the upstream URL | Verify the upstream URL is reachable from the server (not just the browser). Common with private networks or auth-protected CDNs. Next 16 blocks optimization of local-IP upstreams by default — set `images.dangerouslyAllowLocalIP: true` for private networks |
| Blurry or low-quality images | Wrong `sizes` prop or default quality | Set `sizes` to match actual display size. In Next 16, `quality` values other than 75 require `images.qualities` config — the prop is coerced to the closest configured value, so raising `quality` alone silently no-ops |
**Debugging tip:** When images appear broken with no error, always check `/_next/image` requests in the browser Network tab — the HTTP status reveals whether the issue is upstream auth (403), missing config (400), or network (502/504).
## Performance Issues
| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| Slow initial load | Large client bundle | Move components to Server Components; use `dynamic()` for heavy libraries |
| Layout shift on load | Images without dimensions | Add `width`/`height` to `<Image>` or use `fill` |
| Flash of unstyled text | External font stylesheet | Use `next/font` instead of `<link>` |
| Slow navigation | No prefetching | Ensure `<Link>` is used (auto-prefetch); check `prefetch={false}` isn't set |
| High memory in production | Memory leaks in proxy | Avoid storing state in the proxy closure; check for global variable accumulation |
## Deployment Issues
For platform-specific deployment issues (Vercel, Dokploy, Netlify, etc.), see the respective deployment plugin (`vercel-dev`, `dokploy-dev`). Common cross-platform issues:
| Issue | Fix |
|-------|-----|
| Build fails with memory error | Add `NODE_OPTIONS=--max_old_space_size=4096` to build env |
| Environment variables not available at build | Use `NEXT_PUBLIC_` prefix for client-side vars; redeploy after adding env vars |
| API routes timing out | Increase function timeout in your platform settings or stream the response; `export const runtime = 'edge'` only where the platform supports it |
| `revalidateTag` deprecation warning | Add a cacheLife profile argument (`revalidateTag(tag, 'max')`) or use `updateTag()` in Server Actions |
| `output: 'standalone'` not generating `server.js` | Verify config, clear `.next/`, rebuild |
| Static files not found in standalone | Copy `public/` and `.next/static/` to the standalone output |
| Port conflicts | Set `PORT` env var or use `-p` flag |
| CORS errors | Add headers in `next.config.ts` `headers()` or `proxy.ts` |
## Diagnostic Commands
```bash
# Check Next.js version
npx next --version
# Generate route types + full type check
npx next typegen && npx tsc --noEmit
# Clean build
rm -rf .next node_modules/.cache && npm run build
# Debug prerender errors
npx next build --debug-prerender
# Check bundle (Turbopack analyzer, 16.1+)
npx next experimental-analyze
# Debug with the Node inspector
npx next dev --inspect
```
## When to Escalate
- **Persistent 500 errors**: Check server logs, not just browser. Use `next-devtools-mcp` `get_errors` tool
- **Memory leaks**: Profile with `--inspect` flag and Chrome DevTools
- **MCP connection failures**: Verify Next.js 16+, dev server running, and `next-devtools-mcp` in `.mcp.json`
- **Webpack/Turbopack crashes**: Turbopack is the default in 16 — reproduce with `next dev --webpack` / `next build --webpack` to isolate bundler issues
- **Deployment-specific bugs**: Test with `next build && next start` locally before deploying
View on GitHub