correctness/server-browser-global · Browser global in server module code
window, document, localStorage accessed in module scope or a load/handler crash SSR with a ReferenceError.
Severity: critical · Category: correctness
What it checks
Flags reads of browser-only globals (window, document, localStorage, sessionStorage, navigator, location, history, screen, matchMedia, requestAnimationFrame, cancelAnimationFrame, IntersectionObserver, ResizeObserver, MutationObserver, alert, confirm, prompt) in code that always runs on the server:
- module scope of a
.svelte.ts/.svelte.jsrunes module or a.svelte<script module>block (crashes when the module is imported on the server), and - SvelteKit route and hooks files: top level,
load/action/endpoint handler bodies, and theinithook (crashes at import or on every request).
Not flagged:
- Code guarded by
browserfrom$app/environment(aliases included) or atypeof window !== 'undefined'check (early-return guards included). - Code inside
onMount,$effect, or ordinary functions, since none of them run at module evaluation. - A bare
typeof window, which never throws. - Names you imported or declared yourself (
const document = …). - Closures nested inside handlers, typically client callbacks.
- Files that export
ssr = falsethemselves.
Why it matters
None of these globals exist in Node. A module-scope window read crashes the server the moment the file is imported; in a load it crashes every SSR request with ReferenceError: window is not defined, a production 500 the compiler never warns about.
How to fix
export function load() {
const stored = localStorage.getItem('filters'); // ❌ ReferenceError on the server
return {};
}
Move the browser access to the client side, into onMount, which never runs on the server:
<script>
import { onMount } from 'svelte';
let stored = $state(null);
onMount(() => {
stored = localStorage.getItem('filters'); // ✅ onMount never runs on the server
});
</script>
Or guard it explicitly:
import { browser } from '$app/environment';
const stored = browser ? localStorage.getItem('filters') : null; // ✅
Mode differences
None. This rule reads source, the same .svelte and .ts files, everywhere it runs. The CLI, the Vite plugin’s build pass, and the live dashboard’s static baseline all report it identically, and the rendered-HTML pass never re-evaluates it. Scoping a run with --route skips it: component-scoped rules have no route to attribute a finding to.
Disabling
Silence a single occurrence with <!-- svelte-vitals-disable-next-line correctness/server-browser-global --> on the line above it, or turn the rule off:
export default {
rules: {
'correctness/server-browser-global': 'off'
}
};