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/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.
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/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/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/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 runs | Browser globals available |
|---|---|
| Server Component render | No |
| Client component render, server pass | No |
| Client component render, after hydration | Yes |
| useEffect and event handlers | Yes |
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
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.
Frequently Asked Questions
Is window is not defined a Next.js specific error?
Does adding use client stop the error?
Can I read localStorage directly in a Client Component render function?
What is the difference between this and a hydration mismatch error?
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.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.