Errors and fixes
window is not defined in Next.js: where it comes from and how to fix it
The server has no `window`. The error appears when a module or component reads browser APIs during import or render. Three fixes, in order of cost.
Key points
- `window` exists only in the browser: any access during the server render fails.
- The first fix is moving the access into `useEffect`, which runs on the client only.
- `dynamic(..., { ssr: false })` only works inside a Client Component.
Short answer: during the server render window does not exist. The error appears whenever a module reads window at import time, or a component reads it in the render body. Move the access into useEffect, or import the component on the client only.
The exact message
ReferenceError: window is not defined
at Object. (/app/lib/analytics.js:3:12)
at Module._compile (node:internal/modules/cjs/loader:1358:14)
The line it points at is almost always at the top level of the file, not inside a function. That detail separates a render error from an import error.
Why it happens
Next.js runs Server Component code and prerenders pages in Node. Node has no window, document, localStorage or navigator. A module that starts an analytics client at import time runs in that context too.
| Where the access sits | Why it fails | Fix |
|---|---|---|
| Module body | runs at build time and on every request | move it into a function the client calls |
| Server component body | runs in Node | make the file a Client Component, or pass the value as a prop |
| Client component body | still prerendered on the server | use an effect |
The fix, in three levels
If the value is only needed after an interaction, useEffect is enough: it runs in the browser and takes no part in the prerender.
"use client";
import { useEffect, useState } from "react";
export function Width() {
const [width, setWidth] = useState(0);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
onResize();
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return {width || "—"};
}
If the whole component depends on the browser, such as a map, an editor or a canvas, load it without SSR. In the App Router, ssr: false is only allowed inside a Client Component.
"use client";
import dynamic from "next/dynamic";
const Editor = dynamic(() => import("./editor"), { ssr: false });
export function EditorShell() {
return ;
}
If the error comes from a library, the clean route is an explicit guard plus a dynamic import: the module is not evaluated until it is needed.
When the fix does not work
A typeof window guard avoids the crash but solves nothing: the value computed on the server still differs from the client one, and you trade the ReferenceError for a hydration mismatch. Moving the access into a function that runs during rendering changes nothing either. What counts is when it runs, not where it is written.
How to prevent it
- Keep browser APIs in one module and import it dynamically every time.
- Add a lint rule that bans
windowin files without"use client". - Test the build, not only the dev server: the prerender is where the problem surfaces.
Where to continue
To start from code that already runs: React components, a dashboard with a shell and every block with a preview. The linked guides are App Router, structure and client/server boundaries and how much a UI library weighs; the useful comparison is copy-paste or headless.
Frequently asked questions
Why does the error only appear at build time?
next build prerenders the pages, so code that reads window at module level runs in Node.
Is a typeof window guard enough?
It avoids the crash but it is not a fix: the server value still differs from the client one and can produce a hydration mismatch.
Can I use useEffect to read window?
Yes. Effects run in the browser only. Start the state with a neutral value and update it inside the effect.
Why does ssr: false fail in a Server Component?
Next.js 15 does not allow ssr: false in a Server Component. Put "use client" in the file that calls dynamic, or wrap the component in a client wrapper.
Does this apply to document and localStorage too?
Yes. Every browser API is missing in Node: the rule is the same, only the name changes.