# Fix: ReferenceError: window is not defined

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

1. **A browser object read during render.** `const width = window.innerWidth` or `useState(localStorage.getItem('theme'))` in a component body.
2. **A browser object read at the top of a module.** `const storage = window.localStorage` outside any function runs as soon as the file is imported on the server.
3. **A library that uses the browser when imported.** Map, chart, rich-text editor and animation libraries often read `window` or `document` at import time.
4. **Browser code in a Server Component.** A file without `'use client'` that uses `document` or `window`.
5. **Browser code in server-only places.** Route handlers, Server Actions, `getServerSideProps` or 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.

```tsx
'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.

```tsx
'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](/guides/nextjs-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.

```tsx
// 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

```ts
// 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:

```ts
// 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](/docs/node/).

What it does not catch:

- **Build-time errors.** When `next build` prerenders a static page and hits `window`, 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 your `catch`.

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](/docs/quickstart/), 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](https://nextjs.org/docs/app/getting-started/server-and-client-components) (Client Components prerender on the server, `client-only`), checked 2 Nov 2026.
- [Next.js: Lazy Loading](https://nextjs.org/docs/app/guides/lazy-loading) (`ssr: false` only in Client Components), checked 2 Nov 2026.
- [Next.js: Prerender Error](https://nextjs.org/docs/messages/prerender-error), checked 2 Nov 2026.
- [Next.js: instrumentation.js and `onRequestError`](https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation), checked 2 Nov 2026.
- [Node.js globals: `localStorage`, `navigator`](https://github.com/nodejs/node/blob/main/doc/api/globals.md) (added and changed versions), checked 2 Nov 2026.
- [React docs: useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) (`getServerSnapshot`), checked 2 Nov 2026.
- [Next.js `onRecoverableError`](https://github.com/vercel/next.js/blob/canary/packages/next/src/client/react-client-callbacks/on-recoverable-error.ts) and [React error codes](https://github.com/facebook/react/blob/main/scripts/error-codes/codes.json) (419), checked 2 Nov 2026.

## Related errors

- [Next.js hydration mismatch](/guides/nextjs-hydration-mismatch/): `Hydration failed because the server rendered HTML didn't match the client`
- [Next.js Server Components render error](/guides/nextjs-server-components-render-error/): `An error occurred in the Server Components render. The specific message is omitted in production builds`
- [React: Invalid hook call](/guides/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](/guides/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](/guides/react-maximum-update-depth-exceeded/): `Maximum update depth exceeded`

All guides: [Fix an error](/guides/).

[Start free](/console/register?utm_source=bugtape_seo&utm_campaign=error-guides). Free plan · No credit card · Set up in minutes. Or [try the live demo](/demo/?from=landing&start=1#capture) first.
