Why `window` and `localStorage` Do Not Exist in Server Components

Server Components run in a Node.js process, not a browser, so window and localStorage are simply undefined there. Here is why, and how to fix it.

6 min read

Server Components run inside a Node.js process on the server, and that process has no browser attached to it. The window and localStorage APIs belong to the Browser Object Model, a set of globals a browser creates for its own JavaScript engine, so a plain Node.js process never creates them at all. Referencing either one inside a Server Component throws a plain ReferenceError, the same way a bare Node.js script would fail if it referenced window.

This has nothing to do with Next.js choosing to restrict an API. The App Router simply runs Server Component code somewhere a browser window has never existed, so there is no global object carrying that name for the engine to find.

App.tsxApp.tsx
// app/ui/theme-badge.tsx
export default function ThemeBadge() {
  const theme = window.localStorage.getItem('theme') ?? 'light'
  return <span>Theme: {theme}</span>
}

ThemeBadge has no client directive, so it is a Server Component by default, and this file runs only on the server. Rendering the page that imports it fails during that server render pass, because the browser global it reaches for does not exist in a Node.js process.

texttext
ReferenceError: window is not defined
    at ThemeBadge (app/ui/theme-badge.tsx:3:17)

Why the error happens

A Server Component's render function executes once, on the server, to produce either the initial HTML or the React Server Component payload that describes it. No browser is ever involved in that render, so the JavaScript engine running the code has no window object to expose as a global, and no local storage attached to a window that does not exist.

Local storage makes this even clearer, because it is not a separate API on its own. It is a property reached through the window object, so anything that fails to find window also fails to find local storage, navigator, and document for the exact same reason. For the precise reproduction and full message text of this error, see Fixing "ReferenceError: window is not defined" in Next.js.

The fix: move the browser code into a client component

Browser APIs are only legal in code that Next.js knows will also run in a real browser. Add "use client" to a small component, and read the stored value from inside useEffect instead of at the top of the render function.

App.tsxApp.tsx
// app/ui/theme-badge.tsx
'use client'
import { useEffect, useState } from 'react'
 
export default function ThemeBadge() {
  const [theme, setTheme] = useState('light')
  useEffect(() => {
    setTheme(window.localStorage.getItem('theme') ?? 'light')
  }, [])
  return <span>Theme: {theme}</span>
}

This component renders "Theme: light" on the first pass, both on the server and during the browser's initial hydration, because the effect has not run yet at that point. Once React mounts the component in the browser, the effect runs, reads the real stored value, and updates the badge on screen a moment later. The Next.js guide on the boundary between the two rendering models covers this two-pass behavior in more depth in Server Components vs Client Components in Next.js.

A client directive does not mean browser only

The most common confusion here is treating the client directive as a promise that the code never touches the server again. It only marks a module boundary for the client JavaScript bundle. Next.js still renders that same component once on the server to build the initial HTML, exactly as The use client Directive Explained describes in detail.

App.tsxApp.tsx
// app/ui/theme-badge-broken.tsx
'use client'
 
export default function ThemeBadgeBroken() {
  const theme = window.localStorage.getItem('theme') ?? 'light'
  return <span>Theme: {theme}</span>
}

This file has the directive, so it is a client component, but it still throws the same ReferenceError during the server render pass that produces the first HTML for the page. The directive changed where the file's code is allowed to ship, not when its function body actually runs.

When you cannot wait for an effect

Some code needs a browser value before the first paint, such as choosing a class name to avoid a flash of the wrong theme. Guard the access instead of calling it unconditionally, since a guarded check never throws even when there is no browser behind it.

App.tsxApp.tsx
// app/ui/stored-value.tsx
'use client'
 
export function getStoredTheme(): string {
  if (typeof window === 'undefined') {
    return 'light'
  }
  return window.localStorage.getItem('theme') ?? 'light'
}

The typeof check never throws, because typeof on an undeclared name returns the string "undefined" instead of raising an error. On the server this function returns the fallback value, and once the same code runs in the browser it returns the real stored theme.

Where the code runsBrowser globals available
Server Component renderNo
Client component render, server passNo
Client component render, after hydrationYes
useEffect and event handlersYes

The table lines up with one rule: browser globals exist only once a real browser is executing the code, which is after hydration finishes or inside a handler that a real click or input actually triggered.

Common mistake

Reading a stored value directly inside a client component's function body, expecting the client directive alone to make it safe, is the most common version of this mistake. That code still executes during the server render pass that produces the initial HTML, so it fails in exactly the same way a Server Component would.

Keep a component's first render free of browser-only values, then update state from an effect once the component has mounted in the browser. See Can Server Components Use Hooks in Next.js for why effect hooks are off limits inside a Server Component in the first place, and why this fix only works once the code has actually crossed into client component territory.

Rune AI

Rune AI

Key Insights

  • Server Components run in a Node.js process on the server, which has no browser, so window and localStorage are undefined there.
  • This is a plain JavaScript ReferenceError, not a Next.js specific error.
  • Adding use client is not enough by itself, because client components still render once on the server for the initial HTML.
  • Read browser values inside useEffect or an event handler, since those only run in the browser after mount.
  • Use a typeof window check when browser access has to happen outside useEffect or an event handler.
RunePowered by Rune AI

Frequently Asked Questions

Is window is not defined a Next.js specific error?

No. It is a plain JavaScript ReferenceError. window and localStorage are browser globals defined by the Browser Object Model, and they do not exist in any Node.js process, including the one that renders Server Components.

Does adding use client stop the error?

It fixes the error only if the browser code also moves inside a useEffect or an event handler. A Client Component still renders once on the server for the initial HTML, so window at the top level of its render function fails there too.

Can I read localStorage directly in a Client Component render function?

Not safely. The server render pass has no window, so localStorage throws during that pass. Read it inside useEffect instead, since useEffect only runs after the component mounts in the browser.

What is the difference between this and a hydration mismatch error?

This is a ReferenceError because the variable does not exist at all during server rendering. A hydration mismatch is a different problem, where the server HTML and the first client render disagree despite both completing successfully.

Conclusion

window and localStorage are browser globals, and Server Components never run in a browser, so referencing them there is a plain ReferenceError, not a framework restriction. Moving that code into a client component only fixes half the problem, since client components are also rendered on the server once before hydration. Read browser APIs inside an effect hook or an event handler, or guard the access with a typeof check, so the code only runs once a real browser environment exists.