JavaScript Runtime · Reference Cheat Sheet

node

v24 LTS · Krypton V8 · libuv

The original server-side JavaScript runtime: V8 to run your code and libuv’s event loop for fast, non-blocking I/O — a rich set of core modules and the npm ecosystem. Single-threaded, event-driven, and now running TypeScript natively.

run & CLI modules (CJS/ESM) async & core I/O utility modules setup / globals new / advanced gotcha ★ most common
Verified by running Node in-container (event-loop ordering, node:test, http, EventEmitter, stream pipeline, node:sqlite, --run, native TS) & cross-checked against nodejs.org docs + release notes. Targets Node 24 LTS; Node 26 = Current.

The mental model — one JS thread, an event loop, and libuv doing the I/O

A · WHAT’S INSIDE node Your JS / TS runs on ONE thread (TS types stripped first) app.js / .mjs / .ts V8 engine compiles + executes JS → machine code Chrome’s engine libuv event loop + thread pool async, non-blocking I/O C library, off-thread OS files · sockets DNS · timers Batteries: core modules (no install) fs http path events stream crypto os url util buffer net child_process worker_threads node:sqlite B · THE EVENT LOOP (one tick, repeating) timerssetTimeout/Interval pendingdeferred callbacks pollI/O: fs, net, http checksetImmediate close'close' callbacks ↻ loop repeats while work remains MICROTASKS drain after EACH callback process.nextTick > Promises C · ORDERING, PROVEN (real output captured this build) node ordering.js console.log("1 sync") setTimeout(…"5 timeout") setImmediate(…"6 immediate") Promise.resolve().then(…"4 promise") process.nextTick(…"3 nextTick") console.log("2 sync end") 1 sync 2 sync end 3 nextTick 4 promise (microtask) 5 timeout 6 immediate order: sync → nextTick → promise → timers → immediate Three rules that follow Sync code runs first, to completion — nothing async interrupts it. Microtasks (nextTick, Promises) run before the next timer or I/O callback. ● A *Sync call or a heavy loop blocks the one thread — every request waits. ⇒ keep handlers async; offload CPU to worker_threads.

Key insight: your JavaScript runs on a single thread. When you await file or network I/O, Node hands it to libuv and keeps serving other work; the result comes back as a callback the event loop picks up. Block that thread (a *Sync call, a tight loop) and everything stalls.

Part I

Runtime & CLI

01Install & versions
  • nvm install 24 nvm use 24 # or fnm / volta / asdf
    Use a version manager; Node 24 = Active LTS “Krypton”.
  • node -v node --version # v24.x
  • echo "24" > .nvmrc # pin per-project
  • package.json "engines": { "node": ">=24" }
    Declare the supported range.
  • LTS (even) for production · Current (odd) for testing.
    Node 26 = Current; 22 = Maintenance.
02Run & REPL
  • node app.js node app.mjs node app.ts
    .ts runs natively (type stripping).
  • node -e 'console.log(1+2)' node -p '2**10' # eval / print
  • node # interactive REPL (.help .load .save)
  • node --watch app.js # restart on change (no nodemon)
  • echo 'console.log(1)' | node # read from stdin
  • node --env-file=.env app.js # load .env (no dotenv)
03Scripts & env
  • node --run build # run a package.json script (no npm)
    Faster; skips npm’s overhead.
  • node --run # list available scripts
  • NODE_ENV=production node app.js
  • process.env.PORT ?? 3000
    Read env with a default.
  • node --env-file=.env --env-file=.env.local app.js
    Multiple files, later wins.
04Native TypeScript
⚑ Node 24 — stable v24.12
  • node script.ts # strips types, runs — no tsc, no ts-node
    Via Amaro (SWC WASM). Whitespace-replaced, line numbers intact.
  • Type stripping only — Node does not type-check.
    Run tsc --noEmit in CI for real safety.
  • --experimental-transform-types
    Needed for enums, namespaces, param-properties.
  • Erasable-only: use as const objects over enums; explicit .ts import paths.
  • node --experimental-strip-types x.ts # pre-24 / opt-in
05Test runner & debug
⚑ built-in
  • node --test # runs *.test.js (no jest)
    Import from node:test + node:assert.
  • test("adds", () => assert.strictEqual(2+2,4))
  • node --test --watch --test-coverage
  • node --test --test-name-pattern="user"
  • node --inspect app.js # Chrome DevTools debugger
  • node --prof / --cpu-prof # profiling
06Permission model
⚑ Node 24 — stable
  • node --permission app.js # deny-by-default sandbox
    Restrict what deps can touch.
  • --allow-fs-read=./data · --allow-fs-write=./tmp
  • --allow-net · --allow-child-process · --allow-worker
  • process.permission.has("fs.read", "./x")
    Check at runtime.
  • Without a matching --allow-*, the op throws ERR_ACCESS_DENIED.
Part II

Modules & Globals

07CommonJS (CJS)
  • const fs = require("node:fs")
    Synchronous, cached require.
  • module.exports = { a, b } / exports.a = ...
  • __dirname · __filename · require.resolve("pkg")
    CJS-only globals.
  • Default when no "type" & extension is .js / .cjs.
  • require(esm) now works for ESM without top-level await.
08ES Modules (ESM)
  • import fs from "node:fs"; export const x = 1
    The modern default.
  • Enable via "type":"module" or the .mjs extension.
  • import.meta.url · import.meta.dirname · import.meta.filename
    ESM replacements for __dirname.
  • const m = await import("./x.js") — dynamic import.
  • Top-level await is allowed in ESM.
  • Relative imports need the extension: ./util.js.
09package.json essentials
  • "type": "module" — sets CJS vs ESM for .js
  • "main" / "exports" — entry & subpath map
    Modern exports gates what’s importable.
  • "scripts": { "start": "node ." }
    Run via node --run start.
  • "bin" — CLI executables · "engines" — node range
  • "imports": { "#db": "./src/db.js" }
    Internal #-aliases.
10process & argv/env
  • process.argv — [node, script, ...args]
  • process.env.KEY · process.cwd() · process.platform
  • process.exit(1) · process.exitCode = 1
    Prefer setting exitCode.
  • process.on("SIGINT", ...) · process.on("exit", ...)
  • process.stdout.write() · process.nextTick(fn)
  • process.hrtime.bigint() — high-res timing
11Global APIs (no import)
  • fetch(url) · WebSocket · FormData · Blob
    Web-standard, all global now.
  • structuredClone(obj) — deep clone
  • AbortController + signal — cancel fetch / streams
  • URL · URLSearchParams · TextEncoder/Decoder
  • setTimeout / setInterval / setImmediate / queueMicrotask
  • console.log / .error / .table / .time / .dir
12path & url
  • path.join("a","b") · path.resolve()
    Never concatenate paths by hand.
  • path.basename / .dirname / .extname / .parse()
  • path.sep · node:path/posix / win32
    Cross-platform.
  • new URL("./x", import.meta.url)
    Resolve relative to a module.
  • import { fileURLToPath } from "node:url"
    file:// URL → path.
Part III

Async & Core I/O

13The event loop
  • One thread runs your JS; libuv handles I/O off-thread.
    “Don’t block the event loop.”
  • Phases: timers → pending → poll → check → close.
  • process.nextTick() & Promises drain between phases.
    Microtasks beat macrotasks.
  • setImmediate (check) vs setTimeout(fn,0) (timers).
  • queueMicrotask(fn) — schedule a microtask.
  • CPU-bound work? Offload to worker_threads.
14Async patterns
  • Error-first callbacks: fn(err, data) => {}
    The classic Node convention.
  • const d = await readFile(p, "utf8") — async/await
    The modern default.
  • import { promisify } from "node:util"
    Wrap callback APIs into promises.
  • Promise.all / allSettled / race / any
  • Always try/catch awaits or attach .catch()
    Unhandled rejections crash the process.
15fs — file system
  • import { readFile, writeFile } from "node:fs/promises"
    The await-friendly API.
  • await readFile("f.txt", "utf8") · await writeFile(...)
  • fs.readFileSync() — blocks; scripts only, not servers.
    Blocks the whole event loop.
  • fs.readFile(path, cb) — callback flavour
  • await mkdir(p, { recursive: true }) · rm(p, { recursive })
  • createReadStream / createWriteStream for big files.
  • fs.watch() · fsPromises.glob()
16EventEmitter
  • import { EventEmitter } from "node:events"
    Core of streams, http, process…
  • e.on("data", fn) · e.once(...) · e.off(...)
  • e.emit("data", payload)
  • Always handle the "error" event.
    An unhandled error throws.
  • await once(e, "open") — promisified wait
  • new EventEmitter({ signal }) — AbortSignal support
17Streams
  • Four kinds: Readable · Writable · Duplex · Transform.
    Process data chunk-by-chunk.
  • import { pipeline } from "node:stream/promises"
    Prefer over .pipe().
  • await pipeline(src, transform, dest)
    Auto error-handling + cleanup.
  • Readable.from(iterable) · async generators as transforms
  • Backpressure: a slow sink pauses the source automatically.
  • for await (const chunk of readable) {}
    Async iteration.
18HTTP
  • import { createServer } from "node:http"
  • createServer((req, res) => res.end("ok")).listen(3000)
  • res.writeHead(200, { "content-type": "application/json" })
  • Client: use global fetch(url) — simplest.
  • http.request() / http.get() for low-level control.
  • node:http2 · node:https · Undici for HTTP/3.
Part IV

Utility Core & npm

19Buffer · crypto · os · util
  • Buffer.from("hi") · Buffer.alloc(n) — binary data
  • crypto.randomUUID() · crypto.randomBytes(16)
  • createHash("sha256").update(x).digest("hex")
  • os.cpus() · os.platform() · os.totalmem()
  • util.styleText("green", s) — terminal colour
  • util.inspect(obj, { depth }) · util.parseArgs()
20Child processes & workers
⚑ node:sqlite built-in
  • import { spawn, execFile, fork } from "node:child_process"
    Run external programs.
  • spawn("ls", ["-la"]) — streamed; execFile — buffered
  • import { Worker } from "node:worker_threads"
    CPU-bound parallelism.
  • Offload heavy compute to Workers — keeps the loop free.
  • node:cluster — fork the server across CPU cores
  • import { DatabaseSync } from "node:sqlite"
    Built-in SQLite (experimental).
21npm & ecosystem
  • npm init -y npm install express npm i -D vitest
    npm ships with Node (v11 in Node 24).
  • npm ci # clean, lockfile-exact install (CI)
  • npm run build npm test # or node --run build
  • npx cowsay hi # run a package binary, install-on-demand
  • ^1.2.3 (minor) · ~1.2.3 (patch) — semver ranges
  • Commit package-lock.json; git-ignore node_modules
  • npm ≠ Node: npm is the package manager, Node is the runtime.
22Gotchas & pitfalls
  • Forgetting await — a floating promise.
    Unhandled rejections crash the process.
  • __dirname is not defined in ESM.
    Use import.meta.dirname.
  • Mixing require() + top-level await.
    ERR_AMBIGUOUS_MODULE_SYNTAX — pick one system.
  • A *Sync call in a request handler blocks everyone.
  • Unhandled "error" event on an emitter throws.
    Always attach a listener.
  • ESM relative imports need the .js extension.
  • JSON import needs with { type: "json" }
  • Native TS does not type-check — run tsc --noEmit

Four ideas worth a diagram

The distinctions that trip people up most.

1 · Execution order in practice

Why nextTick and Promises always beat setTimeout.

1 · SYNC runs first, fully console.log 1 sync 2 sync end nothing async interrupts it 2 · MICROTASKS drain completely next 3 nextTick 4 promise nextTick queue, THEN promise queue before any timer 3 · MACROTASKS one per loop tick 5 timeout 6 immediate timers, I/O, setImmediate then microtasks again

2 · CommonJS vs ES Modules

Two module systems — the extension and "type" decide which.

CommonJS · .cjs default, synchronous const fs = require("node:fs") module.exports = {} exports.foo = 1 __dirname ✓ __filename ✓ require is cached & blocking require(esm) now allowed ES Modules · .mjs "type":"module" · the modern default import fs from "node:fs" export const x = 1 await import("./a.js") import.meta.url / .dirname top-level await ✓ needs file extensions in paths

3 · Blocking vs non-blocking

The single rule that decides whether your server scales.

✗ readFileSync() the ONE thread req A ▌▌▌ reading disk ▌▌▌ req B ··· waiting ··· req C ··· waiting ··· Everything stalls until the read finishes. Throughput dies. also: crypto, big JSON.parse, tight loops ✓ await readFile() thread stays free req A → libuv pool (off-thread) req B → served now ✓ req C → served now ✓ I/O runs off-thread; the loop keeps serving. Callback returns. this is why Node scales on I/O

4 · Streams & backpressure

Move data in chunks — never load a huge file into memory.

Readable source (file, req) chunk Transform gzip, map, parse chunk Writable sink (file, res) backpressure: slow sink → pauses the source await pipeline(src, transform, dest) — handles errors + backpressure for you

Worth memorizing

node app.tsruns TS natively (type-stripping, stable in 24); it does NOT type-check — use tsc --noEmit.
CJS vs ESMrequire/module.exports (.cjs) vs import/export (.mjs or "type":"module").
__dirnameCJS-only; in ESM use import.meta.dirname / import.meta.url.
event-loop ordersync → process.nextTick → Promises → timers → I/O → setImmediate → close.
don't blockavoid *Sync fs/crypto in servers; offload CPU to worker_threads.
node: prefiximport core as node:fs — unambiguous, can't be shadowed by an npm pkg.
fs has 3 flavoursfs.readFile (cb) · fs.readFileSync · fs/promises (await).
prefer pipelinestream.pipeline() over .pipe() — it handles errors + cleanup.
EventEmitter.on/.once/.emit; always handle 'error' or it throws.
globals, no importfetch, WebSocket, Blob, FormData, structuredClone, AbortController.
node --runruns a package.json script without npm · --watch · --env-file=.env.
node --testbuilt-in runner + node:assert — no jest · --test-coverage.
permission modelnode --permission --allow-fs-read=./ --allow-net — deny by default (Node 24).
process basicsprocess.argv · .env · .cwd() · .exitCode · Buffer for binary.
npm ≠ Nodenpm is the package manager; commit package-lock.json, git-ignore node_modules.
Node 24 LTS“Krypton” · V8 13.6 · npm 11 · Undici/HTTP3. Node 26 Current adds Temporal by default.