# ReferenceError: window is not defined

> ReferenceError: window is not defined: Next.js ran browser code on the server. Why a typeof window check causes a hydration error, and the fixes that work.

Source: https://hydration.jscrate.dev/docs/errors/window-is-not-defined
Last updated: 2026-09-18

`ReferenceError: window is not defined` (Next.js, Remix or any server-rendered
React app) means code that needs the browser ran on the server, where
`window` does not exist. It is a server crash, not a hydration mismatch. A
`typeof window` check in render stops the crash but causes a mismatch. Read
browser values in an effect instead.

## The error

The server throws before any HTML is sent, so the message appears in your
terminal and in the framework's error page, not in the browser console:

```text
ReferenceError: window is not defined
ReferenceError: document is not defined
ReferenceError: localStorage is not defined
ReferenceError: self is not defined
```

Each one names a browser global:

- `window is not defined`: Next.js rendered a component or imported a module
  that reads `window` on the server.
- `document is not defined`: Next.js ran code that queries or changes the
  page, such as `document.querySelector` or `document.cookie`.
- `localStorage is not defined`: Next.js ran a storage read during render,
  often in a `useState` initializer.
- `self is not defined`: usually a library bundled for browsers only.

The same crash can stop `next build`, when Next.js pre-renders static pages.

## Why window is not defined: Next.js renders on the server

Every server-rendered page is rendered twice: once in Node.js to produce the
HTML, and once in the browser to hydrate it. Node.js has no `window`,
`document` or `localStorage`, so reading them during the first render throws.

`"use client"` does not change this. It marks a Client Component, which is
still rendered on the server for the first HTML. Only a Server Component
never runs in the browser, and only code in effects and event handlers never
runs on the server.

Three places run on the server:

- **The component body**, including `useState` and `useReducer` initializers.
- **Module-level code** in any file the page imports: a line such as
  `const width = window.innerWidth` at the top of a file runs on import.
- **Libraries that touch `window` when imported**, such as some chart, map and
  editor packages.

## Why typeof window is the wrong fix

The quick fix people reach for is a guard:

```tsx title="components/greeting.tsx"
"use client";

export function Greeting() {
  // Stops the crash, and causes a hydration mismatch.
  const host = typeof window !== "undefined" ? window.location.hostname : "";
  return <p>Hello from {host || "our site"}</p>;
}
```

The server now renders "Hello from our site" and the browser renders "Hello
from example.com". The crash becomes a
[hydration error](https://hydration.jscrate.dev/docs/errors/hydration-failed-server-rendered-html-didnt-match-client):
React throws the server HTML away and renders the component again. React's
own error message lists this exact pattern as the first cause.

|              | `window is not defined`                | Hydration mismatch                                   |
| ------------ | -------------------------------------- | ---------------------------------------------------- |
| Where        | On the server, before any HTML is sent | In the browser, after the HTML arrived               |
| What you see | A server error page, a failed build    | The page renders twice, content flashes              |
| Cause        | A browser global read on the server    | The server and the browser rendered different output |

## How to fix it

1. **Read browser values in an effect.** Effects run only in the browser,
   after hydration, so both renders start from the same value:

   ```tsx title="components/greeting.tsx"
   "use client";

   import { useEffect, useState } from "react";

   export function Greeting() {
     const [host, setHost] = useState<string | null>(null);

     useEffect(() => {
       setHost(window.location.hostname);
     }, []);

     return <p>Hello from {host ?? "our site"}</p>;
   }
   ```

   See [useEffect and two-pass rendering](https://hydration.jscrate.dev/docs/guides/useeffect-two-pass-rendering).

2. **Render a component only in the browser** when it cannot run on the
   server at all, such as a map. In Next.js, load it with `next/dynamic` and
   `ssr: false`, from a Client Component:

   ```tsx title="app/store-locator/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 />;
   }
   ```

   `ssr: false` is not allowed in a Server Component. See
   [next/dynamic with ssr: false](https://hydration.jscrate.dev/docs/guides/next-dynamic-ssr-false), and
   [client-only components](https://hydration.jscrate.dev/docs/guides/client-only-component) for other
   frameworks.

3. **Import browser-only libraries inside an effect**, with
   `await import("…")`, when you only need them after the page loads.
4. **Move module-level browser reads into functions** that run in effects or
   event handlers.
5. **Read what the server can know on the server.** A theme, a language or a
   signed-in user can come from a cookie, so the first render needs no
   browser value. See [storage](https://hydration.jscrate.dev/docs/causes/storage).

## Find every instance

The [ESLint plugin](https://hydration.jscrate.dev/docs/eslint) finds both the crash and the mismatch
before you run the app:

- [`no-browser-global-in-render`](https://hydration.jscrate.dev/docs/rules/no-browser-global-in-render)
  reports `window`, `document`, `navigator` and `location` read during render.
- [`no-storage-in-initial-render`](https://hydration.jscrate.dev/docs/rules/no-storage-in-initial-render)
  reports `localStorage` and `sessionStorage`.
- [`no-window-render-branch`](https://hydration.jscrate.dev/docs/rules/no-window-render-branch) reports the
  `typeof window` guard in render.
- [`no-client-only-initial-state`](https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state)
  reports browser values in `useState` initializers.

```bash
npm install -D eslint-plugin-hydration-proof
```

hydration-proof loads every route: a page whose server render crashed is
reported as [HP9005](https://hydration.jscrate.dev/docs/issues/hp9005) (an error status), or
[HP2006](https://hydration.jscrate.dev/docs/issues/hp2006) when the crash was inside a Suspense boundary
that React then rendered in the browser. The mismatch a `typeof window` guard
leaves behind is reported with the cause
[browser-only API used during render](https://hydration.jscrate.dev/docs/causes/browser-api):

```bash
npx hydration-proof test
```

## Related

- [Browser-only APIs and hydration](https://hydration.jscrate.dev/docs/causes/browser-api)
- [Render a component only on the client](https://hydration.jscrate.dev/docs/guides/client-only-component)
- [The no-window-render-branch rule](https://hydration.jscrate.dev/docs/rules/no-window-render-branch)
- [localStorage in Next.js without errors](https://hydration.jscrate.dev/docs/causes/storage)
- [Fix Next.js hydration errors](https://hydration.jscrate.dev/docs/frameworks/nextjs)
