Fix this error · React and Next.js
Fix: ReferenceError: window is not defined
window, document or localStorage is not defined means browser code ran on the server. Fixes for Next.js with useEffect, dynamic imports and SSR.
Code that needs a browser ran on the server. Next.js renders every page on the server first, including Client Components, and Node.js has no window or document. The fix is to touch browser objects only after the component mounts (in useEffect or an event handler), or to load a browser-only component with dynamic(..., { ssr: false }) from a Client Component.
| Fact | Detail |
|---|---|
| Error | ReferenceError: window is not defined |
| Same problem | ReferenceError: document is not defined, ReferenceError: localStorage is not defined, ReferenceError: self is not defined |
| Where you see it | The terminal running next dev, the next build output, server or function logs |
| Usual cause | window, document or localStorage read at the top of a module or during render, often inside a library |
| Usual fix | Move the code into useEffect, or load the component with dynamic(..., { ssr: false }) |
| BugTape catches it | Yes, at request time, with the Node SDK in onRequestError (Node.js runtime). Build-time failures are not reported |
What the error means
window, document and localStorage exist only in a browser. Next.js runs your components on the server too:
- Server Components run only on the server.
- Client Components (
'use client') also run on the server, to produce the first HTML. The Next.js docs say Client Components are used to prerender HTML on the server.'use client'does not mean "browser only".
When any of that code reads window, Node.js throws ReferenceError: window is not defined. The same happens for document, self and, on older Node.js versions, localStorage.
It can happen at two times. During next build, Next.js prerenders static pages, and the error fails the build. During a request, for a dynamic page, the error fails that page render on the server.
Newer Node.js versions add some browser globals, which changes the message you see. Node.js 21 added navigator. Node.js 25 turned on localStorage by default. Without the --localstorage-file flag, Node.js 25 gives an empty object, so the call fails with TypeError: localStorage.getItem is not a function. From Node.js 26, reading localStorage without the flag throws a DOMException. typeof window === 'undefined' remains the reliable check for "running on the server".
Common causes
- A browser object read during render.
const width = window.innerWidthoruseState(localStorage.getItem('theme'))in a component body. - A browser object read at the top of a module.
const storage = window.localStorageoutside any function runs as soon as the file is imported on the server. - A library that uses the browser when imported. Map, chart, rich-text editor and animation libraries often read
windowordocumentat import time. - Browser code in a Server Component. A file without
'use client'that usesdocumentorwindow. - Browser code in server-only places. Route handlers, Server Actions,
getServerSidePropsor middleware calling a helper written for the browser.
How to fix it
Read browser values in useEffect
Effects run only in the browser, after the component mounts.
'use client';
import { useEffect, useState } from 'react';
export function WindowWidth() {
const [width, setWidth] = useState<number | null>(null);
useEffect(() => {
const update = () => setWidth(window.innerWidth);
update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);
return <p>{width === null ? 'Measuring…' : `${width}px`}</p>;
}Event handlers also run only in the browser, so onClick={() => localStorage.setItem('x', '1')} is safe.
Read localStorage with a server value
useSyncExternalStore takes a third argument, the value to use on the server and during hydration. It avoids the error and keeps the first browser render equal to the server's.
'use client';
import { useSyncExternalStore } from 'react';
function subscribe(onChange: () => void) {
window.addEventListener('storage', onChange);
return () => window.removeEventListener('storage', onChange);
}
export function useStoredTheme() {
return useSyncExternalStore(
subscribe,
() => localStorage.getItem('theme'), // browser
() => null, // server and hydration
);
}Do not choose different markup with typeof window !== 'undefined' in the render body. It avoids the ReferenceError but causes a hydration mismatch. See Next.js hydration mismatch.
Load browser-only components without server rendering
For a library that reads window when imported, import it with next/dynamic and ssr: false. In the App Router this works only inside a Client Component. Next.js shows an error if you use ssr: false in a Server Component.
// app/map/map-loader.tsx
'use client';
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('./map'), {
ssr: false,
loading: () => <p>Loading map…</p>,
});
export function MapLoader() {
return <Map />;
}Then render <MapLoader /> from the page. You can also import the library inside an effect: useEffect(() => { import('some-lib').then((lib) => lib.start()); }, []).
Move module-level browser code into a function
// Before: runs on import, on the server
const storage = window.localStorage;
export const getToken = () => storage.getItem('token');
// After: runs only when called, from the browser
export const getToken = () => window.localStorage.getItem('token');Add import 'client-only' at the top of such a module. If a Server Component imports it by mistake, Next.js reports it as a build error, so the problem shows before deploy.
Keep browser code out of server files
Server Components, route handlers and Server Actions cannot use browser objects. Read what you need from the request instead, such as cookies with cookies() from next/headers, and move interactive code into a Client Component.
How to catch it in production with BugTape
This error happens on the server, so the browser SDK does not see it. The Node SDK does, through onRequestError in instrumentation.ts, which Next.js 15 and later call for errors in pages, layouts, route handlers and Server Actions:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') (await import('@bugtape/node')).init({ service: 'web' });
}
export async function onRequestError(...args: Parameters<typeof import('@bugtape/node').captureRequestError>) {
if (process.env.NEXT_RUNTIME === 'nodejs') await (await import('@bugtape/node')).captureRequestError(...args);
}Install it with npm install @bugtape/node and set BUGTAPE_KEY in your host's environment. The report is titled with the error, such as ReferenceError: window is not defined, and carries the route pattern (/dashboard/[team]), the route type and the stack frames in your own code, which usually point at the line or the library import. captureRequestError waits up to 2 seconds for delivery, so the report is sent before a serverless function is frozen. Details are in the Node.js guide.
What it does not catch:
- Build-time errors. When
next buildprerenders a static page and hitswindow, the build fails. No server is running yet, so there is nothing to report. You see it in the build log. - The Edge runtime. The SDK does not support Edge, and the checks above skip it.
- Errors you catch yourself. Add
bugtape.captureException(err)in yourcatch.
If the failing Client Component sits inside a Suspense boundary (for example under a loading.tsx), React can stop server rendering for that boundary and render it in the browser instead, where window exists. The page may then look fine to users while the server error repeats on every request. The browser then reports Minified React error #419, which Next.js passes to reportError(). If you also run the browser SDK, it reports that as a separate browser issue. The server issue is the one that names the cause.
Repeats from the same route group into one issue with a count. Set release (the SDK fills it from VERCEL_GIT_COMMIT_SHA and other common commit variables) so that when you mark the issue fixed, a report from a newer release reopens it as a regression. Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and propose the change. You review the pull request.
FAQ
Why do I get "window is not defined" in a 'use client' component?
Client Components are still rendered once on the server to produce the first HTML. 'use client' marks where the browser bundle starts. It does not skip server rendering. Read window in useEffect or an event handler, or load the component with dynamic(..., { ssr: false }).
Is if (typeof window !== 'undefined') a good fix?
It works inside effects, event handlers and helper functions. Inside the render body it causes a hydration mismatch when it changes what the component renders, because the server and the first browser render take different branches. Use useEffect or useSyncExternalStore with a server value there.
How do I fix "document is not defined" from a library?
Find the import in the stack trace, then load the component that uses the library with dynamic(() => import('./editor'), { ssr: false }) inside a Client Component. Some libraries also publish a separate server-safe entry point, so check their docs for Next.js.
Why do I get "localStorage.getItem is not a function" instead?
Your server runs Node.js 25 or later, where localStorage exists as a global. Without the --localstorage-file flag, Node.js 25 gives an empty object, so getItem is missing. From Node.js 26 the same read throws a DOMException. Older Node.js versions give localStorage is not defined. The fix is the same on every version: read localStorage only in the browser.
Sources
- Next.js: Server and Client Components (Client Components prerender on the server,
client-only), checked 2 Nov 2026. - Next.js: Lazy Loading (
ssr: falseonly in Client Components), checked 2 Nov 2026. - Next.js: Prerender Error, checked 2 Nov 2026.
- Next.js: instrumentation.js and
onRequestError, checked 2 Nov 2026. - Node.js globals:
localStorage,navigator(added and changed versions), checked 2 Nov 2026. - React docs: useSyncExternalStore (
getServerSnapshot), checked 2 Nov 2026. - Next.js
onRecoverableErrorand React error codes (419), checked 2 Nov 2026.
Related errors
- Next.js hydration mismatch:
Hydration failed because the server rendered HTML didn't match the client - Next.js Server Components render error:
An error occurred in the Server Components render. The specific message is omitted in production builds - React: Invalid hook call:
Invalid hook call. Hooks can only be called inside of the body of a function component. - React: Too many re-renders:
Too many re-renders. React limits the number of renders to prevent an infinite loop. - React: Maximum update depth exceeded:
Maximum update depth exceeded
All guides: Fix an error.
Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.