Runtime#

The runtime is the engine plus the host. The same V8 engine (used by Chrome, Edge, Node, Deno, Bun, Electron) executes the operator’s source after a JIT-compile pass. The host wraps V8 with the APIs the script can call: DOM in the browser, fs / net / crypto in Node, similar but distinct surfaces in Deno and Bun.

For the language itself, see Syntax. For the toolchain that drives the runtime, see Tooling.

Runtimes#

Runtime

Notes

Node.js

The reference. V8 + libuv. CommonJS plus ESM. Largest ecosystem, slowest startup.

Deno

V8 + Rust. ESM only. URL-based imports, permission sandbox by default, deno.json instead of package.json. TypeScript is a first-class input.

Bun

JavaScriptCore + Zig. Node-compatible; ships its own bundler, package manager, test runner, transpiler. Two orders of magnitude faster startup than Node.

Browsers

V8 (Chromium), SpiderMonkey (Firefox), JavaScriptCore (Safari). DOM, Fetch, Web Workers, IndexedDB; no filesystem.

Embedded

JerryScript, Espruino, Quickjs. JavaScript on microcontrollers and IoT.

node --version / deno --version / bun --version / process.versions print runtime identity at runtime.

Modules#

ES Modules (ESM) is the standard. CommonJS (CJS, the original Node format) is still everywhere.

ESM#

// greet.mjs (or .js with "type":"module" in package.json)
export function greet(name) { return `hello, ${name}`; }
export default greet;

// main.mjs
import greet, {greet as greetFn} from "./greet.mjs";

ESM is async-load, statically analysable, supports top-level await, uses URL-style specifiers, and is the default in Deno, Bun, browsers, and Node when "type":"module" is set in package.json.

CommonJS#

// greet.cjs
function greet(name) { return `hello, ${name}`; }
module.exports = greet;

// main.cjs
const greet = require("./greet");

CommonJS is synchronous load, dynamically resolved, no top-level await. Use it when the operator has to, prefer ESM when the operator can.

Interop. Node can import a CommonJS module (it goes through the default export); require of an ESM module is allowed synchronously from CJS as of Node 22.

Resolution#

Node walks node_modules upward from the current file looking for the package name. Subpaths read package.json’s exports map; the bare specifier "foo" resolves to foo/package.json’s main (or exports["."]).

{
  "name": "myscan",
  "type": "module",
  "exports": {
    ".":       "./src/index.js",
    "./util":  "./src/util.js"
  }
}
import scan from "myscan";          // resolves via exports["."]
import util from "myscan/util";     // resolves via exports["./util"]

For local files write the full extension (./util.js) under ESM in Node; CommonJS allowed extensionless imports as a convenience.

package.json#

The metadata file every Node project carries. The operator maintains name, version, type, main / exports, scripts, dependencies, devDependencies, and engines.

{
  "name": "myscan",
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "start":  "node src/index.js",
    "test":   "vitest",
    "lint":   "eslint src/"
  },
  "dependencies": {"undici": "^7.0.0"},
  "devDependencies": {"vitest": "^2.0.0"},
  "engines": {"node": ">=20"}
}

npm run start (or pnpm start, bun start) executes the script.

globalThis and the global object#

globalThis is the universal handle on the global object; window in browsers, global in Node, self in Web Workers. Use globalThis when writing code that must run in more than one host.

globalThis.AbortController ??= class { /* polyfill */ };

ES modules are strict and have their own module scope; top-level var does not pollute the global. Top-level await is allowed and the runtime resumes the rest of the module after it settles.

process#

In Node, process is the handle on the running process: arguments, environment, stdio, exit code, signal handlers.

process.argv;                       // ["node", "script.js", ...]
process.env.HOME;                   // env vars
process.cwd();                      // working directory
process.exit(0);                    // exit with code

process.on("SIGINT", () => { cleanup(); process.exit(130); });
process.on("unhandledRejection", e => log.fatal(e));

Deno exposes the same surface through Deno.*; Bun adds a Bun.* namespace alongside Node compatibility.

The event loop#

Same loop in every runtime. Node layers libuv on top of V8; browsers use the host’s loop. See Concurrency for the task / microtask model.

Avoid blocking the loop. Synchronous file reads, unbounded JSON.parse on a huge buffer, or a long busy loop all freeze every concurrent request.

Garbage collection#

V8 has a generational GC: young objects get collected fast, survivors get promoted to an old space that gets collected less often. Do not call the GC; --expose-gc plus global.gc() exists but is for tests only.

Heap leaks usually come from a long-lived Map / Set that keeps holding references; WeakMap and WeakRef let the GC reclaim values when nothing else references them.

References#