"ReferenceError: window is not defined" in Next.js: How to Fix It
window, document and localStorage only exist in the browser. Next.js renders components on the server first, where they don't exist. Why it happens, and the fixes: useEffect, 'use client', typeof checks, dynamic imports with ssr: false, and libraries that touch window on import.
ReferenceError: window is not defined
ReferenceError: document is not defined
ReferenceError: localStorage is not defined
window, document, localStorage, navigator — these exist only in the browser. Frameworks like Next.js run your components on the server first to produce HTML, and on the server (Node.js) there is no window. (SSR vs CSR vs SSG)
So code that touches window while rendering crashes during server rendering or the build.
Fix 1: use it inside useEffect
useEffect only runs in the browser, after the component has mounted:
'use client'
import { useEffect, useState } from 'react'
export function ThemeToggle() {
const [theme, setTheme] = useState('light')
useEffect(() => {
setTheme(localStorage.getItem('theme') ?? 'light') // ✅ browser only
}, [])
// ...
}
Event handlers (onClick, etc.) also only run in the browser, so window is safe there.
Fix 2: check before using
For utility code that might run in either place:
if (typeof window !== 'undefined') {
window.scrollTo(0, 0)
}
typeof is safe even when the variable doesn't exist; if (window) would itself throw.
Don't use this check to render different output on server and client:
return <p>{typeof window !== 'undefined' ? 'Browser' : 'Server'}</p> // ❌
That makes the server HTML differ from the browser's first render, causing a hydration error. Use useEffect for anything that changes what's displayed. (Next.js hydration errors)
Fix 3: "use client" isn't enough on its own
In the App Router, adding "use client" makes a component interactive in the browser — but client components are still rendered on the server once for the initial HTML. So window in the component body still fails. You still need useEffect (or an event handler). (React Server Components explained)
Fix 4: load a component only in the browser
Some libraries — maps, charts, rich-text editors, anything that touches window as soon as it's imported — can't be server-rendered at all. Load them dynamically with SSR turned off:
'use client'
import dynamic from 'next/dynamic'
const Map = dynamic(() => import('./Map'), { ssr: false })
export default function Page() {
return <Map />
}
In the App Router, ssr: false must be used inside a client component.
Fix 5: import browser-only libraries lazily
If a library only needs to run on an action:
async function exportPdf() {
const { jsPDF } = await import('jspdf') // loaded in the browser on click
// ...
}
In other environments
The same error appears in:
- Node.js scripts and API routes — there's no browser at all. Move the code to the frontend.
- Tests using Node rather than a browser-like environment — configure Vitest or Jest with
jsdomorhappy-dom. (Vitest vs Jest) - Web workers — no
windowordocumentthere either (useself).
Quick reference
| Where your code runs | Safe to use window? |
|---|---|
| Component body (server or client component) | ❌ |
useEffect / useLayoutEffect |
✅ |
| Event handlers | ✅ |
| Server components, API routes, middleware | ❌ |
dynamic(..., { ssr: false }) component |
✅ |
EasySpawn builds and runs your Next.js app on a real server, so server-rendering errors show up in development exactly as they would in production. See how it works or join the waitlist.
Related: Next.js Hydration Errors · React Server Components Explained · What Is Next.js? · localStorage vs sessionStorage vs Cookies
Keep reading
"Unexpected Token < in JSON at Position 0": What It Means and How to Fix It
Your code expected JSON and got HTML — almost always an error page or your app's index.html. Why it happens (wrong URL, 404, server error, SPA fallback, login redirect), how to see what the server actually sent, and how to parse responses safely.
"A Component Is Changing an Uncontrolled Input to Be Controlled": Fix
React warns when an input's value starts as undefined and later becomes a string. What controlled and uncontrolled inputs are, why undefined initial state causes this, and the fixes: initialise with '', use ?? '' for loaded data, and handle checkboxes and selects properly.