- name
- troubleshoot
- description
- Next.js common errors, debugging techniques, and solutions. This skill should be used when the user asks about "Next.js errors", "hydration error", "Next.js not working", "build errors", "debug Next.js", "'use client' errors", "deployment issues", or encounters problems during Next.js development.
# 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