skip to content

Browser vs Node Global Environment

The same language, two very different hosts: which globals a script may assume in a browser tab versus in Node, and how to write code that survives both. Interviewers raise it whenever SSR, universal libraries, or 'why does this crash on the server?' comes up.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

A script whose first line is `const el = document.body` runs fine in a browser tab but throws `ReferenceError: document is not defined` under `node script.js`. Why is the DOM missing in Node, and what globals does Node supply instead?

level: juniorimportance: must knowfreq 78%

answer

  1. language versus host environment
  2. spec defines built-ins, not documents
  3. browser installs DOM, Node installs process
  4. unresolvable reference throws ReferenceError

basics

~20 s

The DOM is not part of JavaScript. ECMAScript defines only the core language and built-ins such as Object, Array and Promise; document and window are supplied by the browser host. Node is a different host and supplies process, Buffer and its module globals instead.

solid answer

~40 s

JavaScript the language, defined by ECMAScript, has no notion of documents, windows or files. It defines syntax plus built-ins like `Object`, `Array`, `JSON`, `Promise`, `Map` and `Math`. Everything else comes from the **host environment** that embeds the engine. A browser tab installs the DOM and other web platform objects — `document`, `window`, `location`, `navigator` — on the global object; Node installs a different set — `process`, `Buffer`, and in CommonJS files `require`, `module`, `__dirname`. So `document` in Node is simply an identifier that resolves to nothing, and an unresolvable identifier reference throws a `ReferenceError` the moment that line runs. The two hosts do overlap on the language built-ins and on a growing set of web-standard extras such as `console`, `setTimeout`, `URL`, `TextEncoder` and `AbortController`, but DOM access is browser-only.

go deeper

for a junior

Be able to say plainly that document and window come from the browser, not from JavaScript, and that Node offers process and its module globals instead. Know that typeof x is the safe way to test for a global that may not exist.

for a middle

Explain the three layers — language, engine, host — and name what each host contributes. Be precise that the failure is a ReferenceError thrown at execution time, which is why a top-level access breaks the whole module import.

for a senior

Show how this drives real incidents: a library touching browser globals during module evaluation takes down a server render. Talk about pushing host access behind functions that only the browser calls, and about diagnosing which host actually executed the failing line.

for a principal

Own the portability contract for shared code: which runtimes the codebase promises to support, whether host access is allowed in library entry points at all, and how that promise is enforced so a single import does not couple server code to the browser platform.

## The language ends where the host begins It helps to separate three layers that people casually call "JavaScript". 1. **The language**, specified by ECMAScript: syntax, semantics, and a fixed catalogue of built-ins — `Object`, `Array`, `Function`, `String`, `Number`, `BigInt`, `Symbol`, `Math`, `JSON`, `Promise`, `Map`, `Set`, `WeakMap`, `RegExp`, `Date`, `Intl`, the `Error` types, `Proxy`, `Reflect`. Nothing here knows what a document, a file or a network socket is. 2. **The engine** (V8, SpiderMonkey, JavaScriptCore) that implements the language. 3. **The host environment** that embeds the engine and decorates the global object with everything else. A browser tab and a Node process are two different hosts. So `document` is not a JavaScript feature that Node forgot to implement. It is a browser-platform object, specified by the WHATWG DOM and HTML standards, and the browser is what installs it. ## What each host puts on the global object A browser tab gives you `window` (plus the aliases `self` and `globalThis`), `document`, `location`, `navigator`, `history`, `alert`, and the rest of the web platform surface. Node gives you `process` (argv, env, exit code, platform), `Buffer`, and — in CommonJS files only — the module wrapper's `require`, `module`, `exports`, `__dirname` and `__filename`. `global` is Node's older name for the global object; `globalThis` is the standard name and works in both hosts. The two hosts share more than juniors expect. `console`, `setTimeout`/`setInterval`/`clearTimeout`, and `queueMicrotask` are in **neither** the ECMAScript spec nor exclusive to browsers — each host provides them. Node has also adopted a long list of web-standard globals over the years: `URL` and `URLSearchParams`, `TextEncoder`/`TextDecoder`, `AbortController`, `structuredClone` (Node 17+), and a global `fetch` (Node 18+). That is why "is `fetch` defined?" is no longer the same question as "am I in a browser?". ## What the error actually means ```js console.log(typeof document); // "undefined" in Node — safe console.log(document); // ReferenceError: document is not defined ``` Evaluating a bare identifier that resolves to no binding anywhere in the scope chain — and is not a property of the global object — throws a `ReferenceError`. It is thrown when that line **executes**, not when the file is parsed. That timing matters: if the access sits at the top level of a module, the error fires during module evaluation, so the whole `import` fails and nothing downstream of it runs. If it sits inside a function, the module imports fine and the error only appears if something calls that function. The mirror image is equally true. A Node script pasted into a browser console fails on `require is not defined` or `process is not defined`, for exactly the same reason in the opposite direction. ## Where this shows up in real work Server-side rendering is the usual trigger. A component or utility module reads `window.innerWidth`, or a library registers a `document` listener at import time; it works in the browser, and then the same module is imported by the server render and crashes with `document is not defined`. Nothing is wrong with the code — it is running under a host that never had a document. The same applies to build steps, test runs and CLI tools: any time your source is executed by Node rather than served to a page. ## How to check safely The only operator that tolerates an undeclared identifier is `typeof`: ```js if (typeof document !== 'undefined') { document.body.classList.add('ready'); } ``` `if (document)` and even `if (document?.body)` both throw, because the reference itself is what fails — optional chaining protects against `null`/`undefined` *values*, not against a missing binding. Reading through the global object also works, since a missing property is merely `undefined`: `globalThis.document`. ## The mental model to carry into the interview Say it in one line: **JavaScript is portable, its host APIs are not.** Language built-ins are the same everywhere; anything that touches a screen, a filesystem or a process is provided by whoever embedded the engine. Once that split is clear, "why does this crash on the server?" stops being mysterious and becomes a question about which host is running the code.

  • Are setTimeout and console part of the ECMAScript specification?
    No. Neither appears in ECMAScript. Timers come from the HTML specification in browsers and from Node's own timers implementation on the server, and `console` is a separate WHATWG standard that both hosts implement. They feel like language features only because every mainstream host happens to provide them.
  • What happens in the opposite direction — pasting Node code into a browser console?
    It fails the same way: `require is not defined`, `process is not defined`, `Buffer is not defined`. Those are Node host globals, and a page never installs them. Bundled browser code sometimes appears to have `process.env`, but that is a build-time substitution, not a real Node process.
  • Which globals can you rely on in absolutely any JavaScript host?
    Only the ECMAScript built-ins and `globalThis`: `Object`, `Array`, `JSON`, `Promise`, `Map`, `Set`, `Math`, `Date`, `RegExp`, the `Error` types, `Symbol`, `BigInt`, `Proxy`, `Reflect`. Everything else — timers, `console`, `fetch`, `URL` — is a host contribution that happens to be widely provided.

saying these in an interview costs you the question

  • Claims the DOM is part of the JavaScript language
  • Says Node just hasn't implemented document yet
  • Thinks window and globalThis are the same in every host
  • Believes typeof document throws when document is undeclared
  • Assumes any code that runs in Chrome runs in Node

context

open as a page

How do you write a JavaScript module that uses browser-only globals such as `document` but is also imported by server code running in Node, so it does not crash on either host?

level: middleimportance: must knowfreq 68%

basics

~20 s

Never touch host globals while the module is being evaluated. Move every browser access inside functions that only the browser calls, and guard those accesses with a capability check such as typeof document !== 'undefined' so importing the module on the server is always harmless.

open as a page

In a Node ES module (a `.mjs` file, or a `.js` file in a package with `"type": "module"`), why is `__dirname` not defined, and how do you get the current file's directory instead?

level: middleimportance: should knowfreq 48%

basics

~20 s

__dirname, __filename, require, module and exports are parameters that Node's CommonJS wrapper injects into each CJS file. ES modules have no such wrapper, so those names do not exist. ESM gets import.meta.url instead — a file: URL string you convert to a path.

open as a page

What does `setTimeout` return in a browser versus in Node, and what code breaks when you assume the two are the same?

level: middleimportance: should knowfreq 42%

basics

~20 s

Browsers return a positive integer id. Node returns a Timeout object with methods such as ref, unref and refresh. Code breaks when it treats the handle as a number — comparing it, serializing it, or storing it as a numeric id in shared state.

open as a page

Node now provides globals that were once browser-only, such as `fetch`, `AbortController`, `URL` and `structuredClone`. How should shared JavaScript code decide what it is allowed to call?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Test for the capability you are about to use, not for the host you think you are on. Checks like typeof fetch === 'function' stay correct as runtimes converge, while inferring a host from window or process goes stale and is wrong in workers, test environments and non-browser, non-Node runtimes.

open as a page