Next.js Debugging Guide
This guide provides a systematic approach to debugging Next.js applications, covering common error patterns, debugging tools, and resolution strategies for both development and production environments.
Common Error Patterns
- Hydration Mismatches
Symptoms:
Warning: Text content did not match. Server: '...' Client: '...' Warning: Expected server HTML to contain a matching <div> in <div> Hydration failed because the initial UI does not match what was rendered on the server
Common Causes:
-
Using Date.now() , Math.random() , or timestamps in render
-
Browser-only APIs accessed during SSR (window , localStorage , document )
-
Conditional rendering based on client-only state
-
Extension-injected HTML elements
-
Invalid HTML nesting (e.g., <p> inside <p> , <div> inside <p> )
Solutions:
// BAD: Causes hydration mismatch function Component() { return <p>Current time: {new Date().toLocaleString()}</p> }
// GOOD: Use useEffect for client-only values 'use client' import { useState, useEffect } from 'react'
function Component() { const [time, setTime] = useState<string>('')
useEffect(() => { setTime(new Date().toLocaleString()) }, [])
return <p>Current time: {time || 'Loading...'}</p> }
// GOOD: Suppress hydration warning for intentional mismatches <time suppressHydrationWarning> {new Date().toLocaleString()} </time>
- Server/Client Component Confusion
Symptoms:
Error: useState only works in Client Components. Add the "use client" directive Error: You're importing a component that needs useEffect. It only works in a Client Component Error: createContext only works in Client Components
Understanding the Boundary:
// Server Component (default in App Router) // - Can't use hooks (useState, useEffect, etc.) // - Can't use browser APIs // - CAN use async/await directly // - CAN access backend resources directly
// app/page.tsx (Server Component) async function Page() { const data = await db.query('SELECT * FROM posts') // Direct DB access return <PostList posts={data} /> }
// Client Component // - CAN use hooks // - CAN use browser APIs // - Must be marked with 'use client'
// components/Counter.tsx 'use client' import { useState } from 'react'
export function Counter() { const [count, setCount] = useState(0) return <button onClick={() => setCount(c => c + 1)}>{count}</button> }
Context Provider Pattern:
// providers/theme-provider.tsx 'use client' import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext<{ theme: string } | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState('light') return ( <ThemeContext.Provider value={{ theme }}> {children} </ThemeContext.Provider> ) }
// app/layout.tsx (Server Component that uses Client Provider) import { ThemeProvider } from '@/providers/theme-provider'
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <ThemeProvider>{children}</ThemeProvider> </body> </html> ) }
- Dynamic Import Issues
Symptoms:
Error: Element type is invalid. Received undefined Module not found errors in production Components rendering as null
Solutions:
// Dynamic import with SSR disabled (for browser-only libraries) import dynamic from 'next/dynamic'
const MapComponent = dynamic(() => import('@/components/Map'), { ssr: false, loading: () => <p>Loading map...</p> })
// Dynamic import with named exports const Modal = dynamic(() => import('@/components/Modal').then(mod => mod.Modal) )
// Dynamic import with error handling const Chart = dynamic( () => import('@/components/Chart').catch(err => { console.error('Failed to load Chart:', err) return () => <div>Failed to load chart</div> }), { ssr: false } )
- API Route Errors
App Router (Route Handlers):
// app/api/users/route.ts import { NextRequest, NextResponse } from 'next/server'
// GET is cached by default - use dynamic to opt out export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams const id = searchParams.get('id')
const data = await fetchUser(id)
return NextResponse.json(data)
} catch (error) { console.error('API Error:', error) return NextResponse.json( { error: 'Internal Server Error' }, { status: 500 } ) } }
export async function POST(request: NextRequest) { try { const body = await request.json() // Validate body... const result = await createUser(body) return NextResponse.json(result, { status: 201 }) } catch (error) { if (error instanceof SyntaxError) { return NextResponse.json( { error: 'Invalid JSON' }, { status: 400 } ) } throw error } }
Common API Route Issues:
// ISSUE: Route Handler cached unexpectedly // SOLUTION: Force dynamic rendering export const dynamic = 'force-dynamic' export const revalidate = 0
// ISSUE: Can't access cookies/headers // SOLUTION: Import from next/headers import { cookies, headers } from 'next/headers'
export async function GET() { const cookieStore = await cookies() const token = cookieStore.get('token')
const headersList = await headers() const userAgent = headersList.get('user-agent') }
// ISSUE: CORS errors // SOLUTION: Add CORS headers export async function GET() { const response = NextResponse.json({ data: 'test' }) response.headers.set('Access-Control-Allow-Origin', '*') response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') return response }
export async function OPTIONS() { return new NextResponse(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }, }) }
- Build Failures
Common Build Errors:
Type errors during build
Error: Type error: Property 'x' does not exist on type 'y'
Solution: Fix TypeScript errors or use type assertions
Module resolution failures
Error: Module not found: Can't resolve '@/components/...'
Solution: Check tsconfig.json paths and next.config.js
Static generation failures
Error: getStaticPaths is required for dynamic SSG pages
Solution: Add getStaticPaths or use generateStaticParams
Image optimization errors
Error: Invalid src prop on next/image
Solution: Configure domains in next.config.js
next.config.js Debugging:
/** @type {import('next').NextConfig} */ const nextConfig = { // Enable verbose build output logging: { fetches: { fullUrl: true, }, },
// Debug image domains images: { remotePatterns: [ { protocol: 'https', hostname: '**.example.com', }, ], },
// Analyze bundle size webpack: (config, { isServer }) => { if (process.env.ANALYZE) { const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') config.plugins.push( new BundleAnalyzerPlugin({ analyzerMode: 'static', reportFilename: isServer ? '../analyze/server.html' : './analyze/client.html', }) ) } return config }, }
module.exports = nextConfig
- Middleware Issues
Symptoms:
Middleware triggered unexpectedly Infinite redirect loops Middleware not executing Edge runtime compatibility errors
Debugging Middleware:
// middleware.ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) { // Debug: Log all middleware invocations console.log('Middleware:', request.method, request.nextUrl.pathname)
// Check if this path should be handled const pathname = request.nextUrl.pathname
// Skip static files and API routes if needed if ( pathname.startsWith('/_next') || pathname.startsWith('/api') || pathname.includes('.') ) { return NextResponse.next() }
// Example: Auth check const token = request.cookies.get('token')?.value
if (!token && pathname.startsWith('/dashboard')) { const url = new URL('/login', request.url) url.searchParams.set('from', pathname) return NextResponse.redirect(url) }
return NextResponse.next() }
// Limit middleware to specific paths export const config = { matcher: [ // Match all paths except static files '/((?!_next/static|_next/image|favicon.ico).*)', ], }
- Caching and Revalidation Issues
Symptoms:
Stale data after updates Pages not revalidating Unexpected cache hits/misses
Solutions:
// Force dynamic rendering (no cache) export const dynamic = 'force-dynamic' export const revalidate = 0
// Time-based revalidation export const revalidate = 60 // Revalidate every 60 seconds
// On-demand revalidation // app/api/revalidate/route.ts import { revalidatePath, revalidateTag } from 'next/cache' import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) { const { path, tag, secret } = await request.json()
if (secret !== process.env.REVALIDATION_SECRET) { return Response.json({ error: 'Invalid secret' }, { status: 401 }) }
if (path) { revalidatePath(path) } if (tag) { revalidateTag(tag) }
return Response.json({ revalidated: true, now: Date.now() }) }
// Using fetch with cache tags async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { tags: ['posts'], revalidate: 3600 } }) return res.json() }
Debugging Tools
- VS Code Debugger Setup
Create .vscode/launch.json :
{ "version": "0.2.0", "configurations": [ { "name": "Next.js: debug server-side", "type": "node-terminal", "request": "launch", "command": "npm run dev" }, { "name": "Next.js: debug client-side", "type": "chrome", "request": "launch", "url": "http://localhost:3000" }, { "name": "Next.js: debug full stack", "type": "node", "request": "launch", "program": "${workspaceFolder}/node_modules/.bin/next", "args": ["dev"], "skipFiles": ["<node_internals>/**"], "serverReadyAction": { "action": "debugWithChrome", "pattern": "- Local:.+(https?://.+)", "uriFormat": "%s", "webRoot": "${workspaceFolder}" } } ] }
- Next.js 16.1+ Debug Flag
Enable Node.js debugger directly
next dev --inspect
With specific port
next dev --inspect=9229
Break on first line
next dev --inspect-brk
- Chrome DevTools
Start with debugging
NODE_OPTIONS='--inspect' npm run dev
Then open in Chrome:
chrome://inspect
- React DevTools
-
Install browser extension
-
Use Components tab to inspect component hierarchy
-
Use Profiler tab to identify re-render issues
-
Enable "Highlight updates when components render"
- Next.js Error Overlay
The development error overlay provides:
-
Full stack traces with source maps
-
Component stack for React errors
-
Quick links to open files in editor
Configure editor integration in next.config.js :
module.exports = { experimental: { // Open files in VS Code serverActions: { bodySizeLimit: '2mb', }, }, }
The Four Phases of Next.js Debugging
Phase 1: Reproduce and Isolate
Steps:
-
Reproduce the issue consistently
-
Determine if it's client-side, server-side, or build-time
-
Check if it occurs in development, production, or both
-
Isolate to a minimal reproduction
Commands:
Test in development
npm run dev
Test production build locally
npm run build && npm run start
Check for TypeScript errors
npx tsc --noEmit
Check for ESLint errors
npm run lint
Phase 2: Gather Information
Client-Side:
// Add debug logging 'use client' import { useEffect } from 'react'
export function DebugComponent({ data }: { data: unknown }) { useEffect(() => { console.log('Component mounted with data:', data) console.log('Window location:', window.location.href) console.log('User agent:', navigator.userAgent) }, [data])
return null }
Server-Side:
// app/page.tsx export default async function Page() { console.log('Server rendering page') console.log('Environment:', process.env.NODE_ENV) console.log('Request time:', new Date().toISOString())
const data = await fetchData() console.log('Fetched data:', JSON.stringify(data, null, 2))
return <div>...</div> }
Request/Response Debugging:
// middleware.ts export function middleware(request: NextRequest) { console.log('=== Request Debug ===') console.log('URL:', request.url) console.log('Method:', request.method) console.log('Headers:', Object.fromEntries(request.headers)) console.log('Cookies:', request.cookies.getAll())
const response = NextResponse.next()
// Add debug header response.headers.set('X-Debug-Time', Date.now().toString())
return response }
Phase 3: Analyze and Diagnose
Check Build Output:
Analyze bundle
ANALYZE=true npm run build
Check static/dynamic pages
npm run build
Look for:
- Static (SSG) pages: filled circle
- Server-rendered (SSR): lambda
- ISR pages: filled circle with revalidation time
Check Network Requests:
// Wrap fetch for debugging const originalFetch = global.fetch global.fetch = async (...args) => { const [url, options] = args console.log('Fetch:', url, options?.method || 'GET')
const start = Date.now() const response = await originalFetch(...args) const duration = Date.now() - start
console.log('Response:', response.status, ${duration}ms)
return response
}
Phase 4: Fix and Verify
Common Fix Patterns:
// Fix 1: Add error boundary // app/error.tsx 'use client'
export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { console.error('Error boundary caught:', error)
return ( <div> <h2>Something went wrong!</h2> <p>{error.message}</p> <button onClick={reset}>Try again</button> </div> ) }
// Fix 2: Add loading state // app/loading.tsx export default function Loading() { return <div>Loading...</div> }
// Fix 3: Handle async errors properly
async function getData() {
try {
const res = await fetch('...')
if (!res.ok) {
throw new Error(HTTP ${res.status}: ${res.statusText})
}
return res.json()
} catch (error) {
console.error('getData failed:', error)
// Return fallback or re-throw
return { error: true, data: null }
}
}
Quick Reference Commands
Development
Start development server
npm run dev next dev
Start with debugging
next dev --inspect NODE_OPTIONS='--inspect' npm run dev
Start with Turbopack (faster HMR)
next dev --turbopack
Clear Next.js cache
rm -rf .next
Building
Production build
npm run build next build
Build with verbose output
NEXT_DEBUG=1 npm run build
Analyze bundle size
ANALYZE=true npm run build
Linting and Type Checking
Run ESLint
npm run lint next lint
Fix ESLint errors
next lint --fix
Type check
npx tsc --noEmit
Type check with watch mode
npx tsc --noEmit --watch
Production Testing
Start production server
npm run start next start
Start on specific port
next start -p 3001
Test production build locally
npm run build && npm run start
Cache Management
Clear all caches
rm -rf .next node_modules/.cache
Clear just build cache
rm -rf .next
Clear fetch cache (App Router)
Add to fetch: { cache: 'no-store' }
Or: export const revalidate = 0
Debugging Specific Issues
Check for circular dependencies
npx madge --circular --extensions ts,tsx ./app
Find large dependencies
npx source-map-explorer .next/static/chunks/*.js
Check bundle composition
npx @next/bundle-analyzer
Environment-Specific Debugging
Development vs Production Differences
// Check environment const isDev = process.env.NODE_ENV === 'development' const isProd = process.env.NODE_ENV === 'production'
// Development-only logging if (isDev) { console.log('Debug info:', data) }
// Production error reporting if (isProd && error) { // Send to error tracking service Sentry.captureException(error) }
Environment Variables
Check env vars are loaded
console.log('Env check:', { NODE_ENV: process.env.NODE_ENV, API_URL: process.env.NEXT_PUBLIC_API_URL, // Note: Only NEXT_PUBLIC_* vars are available client-side })
Performance Debugging
Web Vitals Monitoring
// app/layout.tsx import { SpeedInsights } from '@vercel/speed-insights/next'
export default function RootLayout({ children }) { return ( <html> <body> {children} <SpeedInsights /> </body> </html> ) }
// Custom Web Vitals reporting // pages/_app.tsx (Pages Router) export function reportWebVitals(metric) { console.log(metric.name, metric.value) // Send to analytics }
React Profiler
import { Profiler } from 'react'
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log(${id} ${phase}: ${actualDuration.toFixed(2)}ms)
}
export default function App() { return ( <Profiler id="App" onRender={onRenderCallback}> <MainComponent /> </Profiler> ) }
Sources and References
This guide was compiled using information from:
-
Next.js Official Debugging Guide
-
Next.js 16.1 Release Notes
-
Vercel: Common Next.js App Router Mistakes
-
Next.js Error Handling Documentation
-
8 Tips for Debugging Next.js Applications
-
How to Debug Next.js with VS Code
-
Debugging Next.js Like a Pro
-
10 Common Next.js Bugs and How to Fix Them
-
Next.js 15 Error Handling Best Practices