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(or SvelteKit 3’s$app/env; aliases included) or atypeofcheck such astypeof window !== 'undefined'ortypeof matchMedia === 'function'(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 a literal
ssr = falsethemselves, and theloadfunctions of universal+page.ts/+layout.tsfiles whose route never renders on the server (their module scope is still checked: SvelteKit imports the file on the server when it cannot read the page options statically). A route never renders on the server when the nearestssrexport, looking first at the page’s own+pagefiles and then up its layout chain (following+page@/+layout@resets), isssr = false. Withkit.router.type: 'hash'insvelte.config.js, no route renders on the server.ssr = dev(withdevfrom$app/environment) counts asfalse, its value in every production build, though a file exporting it still has its module scope checked, since SvelteKit imports it on the server to read the option; anssrexported as anything else counts as turning SSR back on. Where a node’s universal module (+page.ts/+layout.ts) and its server module both export it, the universal one wins, as in SvelteKit. A layout qualifies only when every page under it does, and the root layout only when it exportsssr = falseitself. Server files (+page.server.ts,+server.ts,hooks.server.ts) and runes modules are still checked.
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'
}
};