1) Hydration Errors
Typical message:
“Hydration failed because the initial UI does not match what was rendered on the server.”
Why it happens (in plain words):
Next.js renders HTML on the server first (SSR), then React “hydrates” it on the client. If the HTML from the server doesn’t match what the browser renders on load, React complains. Mismatches usually come from non-deterministic values (like Date.now()
, Math.random()
, or reading localStorage
) or client-only components that tried to run on the server.
Quick checklist
-
Are you using random or time-based values in your render?
-
Are you reading browser-only APIs (
window
,localStorage
,document
) during the initial render? -
Is some markup gated by conditions that differ between server and client?
Copy-paste fixes
-
Move browser-only logic into an effect:
// ✅ Safe: runs only in the browser after first paint useEffect(() => { const theme = localStorage.getItem('theme'); setTheme(theme ?? 'light'); }, []);
-
Delay client-only UI until mounted:
const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); if (!mounted) return null; // or a lightweight skeleton
-
In the App Router, if a component must be client-only, put
"use client"
at the top of the file. -
When using third-party libraries that depend on the DOM, load them dynamically:
const ClientOnlyWidget = dynamic(() => import('./Widget'), { ssr: false });
Pro tip:
Watch out for list keys and conditional wrappers. Changing the structure or keys between server and client (e.g., rendering a different number of <li>
items) also triggers hydration errors.
2) “window is not defined
” (and its cousins document
, localStorage
)
Why it happens:window
exists in the browser, not on the server. During SSR, any direct usage will crash.
Spot the pattern
-
Direct reads at the top level:
const x = window.location.href
-
Usage inside render before mount
-
Libraries that assume a browser environment
Copy-paste fixes
-
Guard with
typeof window
:const isBrowser = typeof window !== 'undefined'; const href = isBrowser ? window.location.href : '';
-
Move to
useEffect
:useEffect(() => { // safe browser-only work here console.log(window.innerWidth); }, []);
-
Dynamic import with SSR off for DOM-heavy libs:
const Map = dynamic(() => import('./Map'), { ssr: false });
-
App Router: Mark the component as client:
Pro tip:"use client"; // component code that touches window
Sometimes a deep dependency useswindow
. Load just that widget withssr:false
instead of turning your whole page into client mode.
3) Build Errors (next build
/ production surprises)
a) “Module not found / Can’t resolve …”
-
Cause: Wrong import path, file renamed, or case mismatch (
import x from './Logo'
vs./logo
on case-sensitive systems). -
Fix: Confirm the path, check casing, ensure the file actually exists in prod (Git sometimes ignores case-only changes).
b) TypeScript or ESLint stops the build
-
Cause: Strict configs catch what dev hot-reload lets slide.
-
Fix: Read the first error (others are often symptoms). Either fix the type, add a type guard, or adjust
tsconfig.json
/.eslintrc
to match your team’s tolerance. Avoid blanketany
; prefer narrow type assertions.
c) Environment variable gotchas
-
Symptoms: Works locally, fails in prod; values are
undefined
. -
Fixes:
-
Put secrets in
.env.local
(dev) and your hosting provider’s env panel for prod. -
Client-side usage must start with
NEXT_PUBLIC_
:NEXT_PUBLIC_API_URL=https://api.example.com
-
After changing envs, restart the server; rebuild for production.
-
d) Image & asset hiccups
-
next/image
external domains: Add them tonext.config.js
:images: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }] }
-
Static assets: Place under
/public
and reference/my-file.png
.
e) Node / package mismatches
-
Cause: Using a Node version outside Next’s supported range, or mismatched React/Next versions.
-
Fix: Use the current LTS Node for your project. Delete
node_modules
+ lockfile, then reinstall:rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml npm i # or yarn / pnpm
f) “Cannot use import statement outside a module”
-
Cause: Config conflict (ESM vs CJS) in a dependency or tool script.
-
Fix: Ensure your
type
inpackage.json
matches your code style ("type":"module"
for ESM). For server utils that need CJS, use.cjs
extensions or update the import syntax accordingly.
A Calm Debugging Flow (save this!)
-
Reproduce in a clean state: Stop dev server, clear cache, restart. Try
next build
to see the real error stack. -
Minimal example: Comment out sections until the error disappears. The last change points to the culprit.
-
Server vs client: Ask, “Does this line need the browser?” If yes, move it to
useEffect
or a client-only component. -
Determinism check: Remove randomness/time from the initial render. Replace with placeholders and fill after mount.
-
One fix at a time: Change, test, commit. Don’t blend three fixes—you’ll never know which one worked.
Copy-Ready Snippets
Client-only wrapper:
"use client";
export default function ClientOnly({ children }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return mounted ? children : null;
}
Dynamic import for DOM libraries:
const Chart = dynamic(() => import('./Chart'), { ssr: false });
Safe localStorage read:
const [theme, setTheme] = useState('light');
useEffect(() => {
try {
const saved = localStorage.getItem('theme');
if (saved) setTheme(saved);
} catch {}
}, []);
Final Thoughts
Next.js errors feel scary until you see the pattern: What ran on the server that should have waited for the browser? or What changed between server and client? Apply the guards above, keep your first render deterministic, and lean on dynamic imports for DOM-heavy pieces. With that mindset, hydration warnings, window
blow-ups, and stubborn build breaks turn from blockers into quick fixes.
If you want, share the specific error text and a small code snippet—I’ll tailor the exact fix to your setup.