Node.js vs Browser JavaScript
Node.js and the browser both run JavaScript, but they are different host environments with different global objects, APIs, security models, and assumptions. The language is identical; the surroundings are not. Understanding precisely where they differ eliminates a whole class of "why is document undefined?" and "why can't I read this file?" confusion — and tells you which code can be shared between front end and back end.
Same language, different host
Both run an engine (the browser and Node.js both use V8 in Chrome's case). The engine implements ECMAScript: variables, functions, classes, objects, Math, JSON, promises, async/await. Everything beyond the pure language is supplied by the host — and that is where they diverge.
Capability | Browser | Node.js |
|---|---|---|
Global object |
|
|
The DOM ( | Yes | No — there is no page |
File system access | No (sandboxed) | Yes ( |
Raw network sockets | No | Yes ( |
Make HTTP requests |
|
|
Module system | ES Modules only | CommonJS and ES Modules |
Environment variables | No |
|
Spawn OS processes | No |
|
Multithreading | Web Workers | Worker Threads, cluster, child processes |
Binary data |
|
|
Persistent storage |
| Files, databases |
Security boundary | Same-origin policy, sandbox | Full OS access (as the user) |
Primary concern | UI and user interaction | I/O, servers, tooling |
What the browser has that Node lacks
There is no DOM in Node.js. document, window, alert, localStorage, navigator, and the entire UI surface do not exist — there is no web page for them to act on. Code that manipulates the page cannot run in Node unmodified.
Browser-only — these throw ReferenceError in Node
document.querySelector('h1').textContent = 'Hi'
window.localStorage.setItem('token', 'abc')
window.addEventListener('resize', onResize)What Node has that the browser lacks
Node-only capabilities
import fs from 'node:fs'
// Read a file from disk — impossible in a sandboxed browser.
const config = fs.readFileSync('config.json', 'utf8')
// Inspect the process and environment.
console.log(process.env.NODE_ENV) // 'production'
console.log(process.platform) // 'linux'
console.log(__dirname) // current directory (CommonJS)
// Open a TCP server — the browser can never do this.
import net from 'node:net'
net.createServer(socket => socket.end('hello\n')).listen(7000)Globals compared
Use globalThis for code intended to run in both environments — it resolves to window in the browser and global in Node. Node additionally injects CommonJS-only names — __dirname, __filename, require, module, exports — which do not exist in ES modules. In ESM, derive the path values from import.meta.url instead:
ESM equivalent of __dirname
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)The shrinking gap: shared Web Platform APIs
Modern Node.js has deliberately adopted many Web Platform APIs so that code is more portable and developers face fewer "this works in the browser but not Node" surprises. Available in recent Node versions:
fetch,Request,Response,Headers— the same HTTP client you use in the browser.URLandURLSearchParams.TextEncoder/TextDecoder.structuredClonefor deep cloning.AbortController/AbortSignalfor cancellation.Web Streams (
ReadableStream,WritableStream) alongside Node streams.Web Crypto(crypto.subtle),performance,queueMicrotask, and timers.
Isomorphic / universal code
"Isomorphic" (or "universal") code runs unchanged on both the server and the client — the foundation of frameworks like Next.js and Remix. The rule of thumb:
Pure logic — validation rules, date math, formatting, business calculations, pricing — touches neither the DOM nor the file system, so it can be shared freely.
Environment-specific code — DOM access,
fs,process— must be kept on the correct side, or hidden behind an abstraction that has a browser implementation and a Node implementation.Bundlers (Vite, webpack, esbuild) and the
package.json"exports"field with"browser"/"node"conditions let a library ship different code to each environment.
Guarding environment-specific code
// A shared module that adapts at runtime
const isNode =
typeof process !== 'undefined' && process.versions?.node != null
export function getStorage() {
return isNode ? new FileStorage() : new LocalStorageAdapter()
}