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?
answer
- language versus host environment
- spec defines built-ins, not documents
- browser installs DOM, Node installs process
- unresolvable reference throws ReferenceError
basics
~20 sThe 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 sJavaScript 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
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.
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.
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.
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