Back to cheat sheets

Languages

Node.js

The JavaScript runtime: event loop and non-blocking I/O, CommonJS vs ES modules, async patterns, streams, EventEmitter, error handling, and scaling with cluster and worker threads — with rapid-fire Q&A.

01Runtime & Event Loop

Node.js runs JavaScript outside the browser on Google's V8 engine, paired with libuv — a C library that provides the event loop and a thread pool for I/O. Node is single-threaded for your JavaScript, but I/O is offloaded so it never blocks.

Node's model is single-threaded, non-blocking, event-driven. One thread runs your JS; libuv handles file, network, and timer work in the background and queues callbacks for the loop to run.

The event loop processes work in phases, in order, each draining its queue:

  1. timerssetTimeout / setInterval callbacks.
  2. pending callbacks — deferred I/O callbacks.
  3. poll — retrieve new I/O events; execute their callbacks.
  4. checksetImmediate callbacks.
  5. closeclose events (e.g. a socket closing).
Microtasks run between phases: process.nextTick fires first, then resolved Promise callbacks — both drain fully before the loop moves on. That's why a runaway nextTick can starve I/O.

02Modules: CommonJS vs ES Modules

Node supports two module systems:

AspectCommonJS / ES Modules
Syntaxrequire() / module.exports  —  import / export
LoadingSynchronous require / asynchronous & statically analyzable
Default in.cjs (or no type field) / .mjs (or "type": "module")
Top-level awaitNo / Yes
A package's "type" field in package.json decides how bare .js files are interpreted — "commonjs" (default) or "module". ESM can import CommonJS; CommonJS historically loaded ESM only via dynamic import(), though recent Node added synchronous require() of ES modules.

03Async Patterns

Three generations of asynchronous style, each built on the last:

Callbacks (error-first)

The original Node convention: fn(args, (err, result) => ...) — the first argument is always the error. Nesting many of them creates "callback hell".

Promises

An object representing a future value — .then() / .catch(), chainable and composable. Promise.all / allSettled / race coordinate concurrency.

async / await

Syntactic sugar over Promises — write asynchronous code that reads sequentially, with normal try/catch for errors. The modern default.

Wrap a callback API into a Promise with util.promisify, or use the promise variants of core modules (fs/promises). Run independent async work concurrently with Promise.all rather than awaiting each in sequence.

04Streams & Buffers

Streams process data in chunks instead of loading it all into memory — essential for large files and network data. Four types:

  • Readable — source you read from (fs.createReadStream, an HTTP request).
  • Writable — destination you write to (a file, an HTTP response).
  • Duplex — both readable and writable (a TCP socket).
  • Transform — a duplex stream that modifies data passing through (gzip, encryption).

Connect them with pipe() or, preferably, stream.pipeline(), which propagates errors and cleans up:

const { pipeline } = require("node:stream/promises");
await pipeline(
  fs.createReadStream("in.txt"),
  zlib.createGzip(),
  fs.createWriteStream("out.txt.gz")
);
Backpressure: when a writable can't keep up, pipe/pipeline automatically pause the readable so memory stays bounded. A Buffer holds raw binary data — the chunks streams move around.

05EventEmitter

Much of Node's core is built on the EventEmitter pattern — objects that emit named events and call registered listeners. Streams, HTTP servers, and sockets are all emitters.

const { EventEmitter } = require("node:events");
const bus = new EventEmitter();
bus.on("order", (id) => console.log("processing", id));
bus.emit("order", 42);
  • on(event, fn) / once(event, fn) — subscribe (persistent / one-shot).
  • emit(event, ...args) — fire synchronously to all listeners in registration order.
  • off() / removeListener() — unsubscribe to avoid leaks.
An "error" event with no listener throws and can crash the process — always attach an error listener to emitters that can fail.

06Error Handling

Error handling depends on the async style:

  • Sync & async/awaittry/catch.
  • Callbacks — check the error-first argument every time.
  • Promises.catch(); an unhandled rejection emits process.on("unhandledRejection").
  • EventEmitters — listen for the "error" event.
uncaughtException is a last resort, not a recovery tool. After one, the process is in an undefined state — log, run critical cleanup, and exit. Let a process manager (pm2, systemd, Kubernetes) restart it rather than trying to keep running.

07Concurrency & Scaling

One Node process uses one CPU core for JS. To use more — or to keep the loop responsive under heavy compute — Node offers three tools, chosen by the kind of work:

cluster

Forks multiple Node processes that share a listening port; the primary accepts connections and hands them to workers (round-robin by default on Linux/macOS, OS-scheduled on Windows). Scales an I/O-bound server across all cores.

worker_threads

Real threads inside one process, sharing memory via SharedArrayBuffer. The right tool for CPU-bound work (parsing, image processing) that would otherwise block the event loop.

child_process

spawn / exec / fork to run separate programs or Node scripts and communicate over IPC or streams.

Rule of thumb: I/O-bound and need more throughput → cluster; CPU-bound and blocking the loop → worker_threads.

08npm, Core Modules & Tooling

npm manages dependencies via package.json; package-lock.json pins exact versions for reproducible installs. Versions follow semver (MAJOR.MINOR.PATCH): ^1.2.3 allows minor/patch updates, ~1.2.3 only patch.

Frequently used built-in (node:) modules:

  • fs / path — files and cross-platform paths.
  • http / https — servers and clients (plus the global fetch).
  • os / process — system info, env vars (process.env), arguments.
  • crypto — hashing, HMAC, encryption.
  • node:test + node:assert — the built-in test runner, no dependency needed.
Prefer dependencies for runtime needs and devDependencies for build/test tooling. Use the node: prefix on core imports to make intent explicit and avoid clashes with npm packages.

09Rapid-Fire Q&A

Reveal each answer to self-check, then test yourself with the quiz.

How does single-threaded Node.js handle many concurrent I/O operations without blocking?

Your JS runs on one thread, but libuv performs I/O in the background and queues callbacks; the event loop runs them when the operation completes — non-blocking, event-driven concurrency.

What runs between event-loop phases, before the loop continues?

Microtasks drain between phases — process.nextTick callbacks first, then the Promise microtask queue — which is why a runaway nextTick can starve I/O.

What is the key difference between CommonJS and ES modules in Node?

CommonJS is synchronous require/module.exports; ESM is statically analyzable import/export and supports top-level await. The package.json "type" field decides how bare .js files are treated.

For a CPU-bound task that would block the event loop, what should you reach for?

worker_threads run real threads (with shared memory) for CPU-bound work so the main event loop stays responsive. cluster scales I/O-bound servers across cores but doesn't unblock a single heavy computation.

Why use stream.pipeline() instead of chaining .pipe() calls?

pipeline forwards errors from any stage and destroys all streams on failure, avoiding leaks; chained pipe() calls swallow downstream errors and require manual cleanup. Both preserve backpressure.

What happens when an EventEmitter emits an 'error' event with no listener attached?

An 'error' event with no listener is thrown as an exception and can terminate the process — always attach an error listener to emitters that can fail.

What is the recommended way to handle process.on('uncaughtException')?

After an uncaughtException the process is in an undefined state. Log, do essential cleanup, and exit; rely on a process manager (pm2, systemd, Kubernetes) to restart — don't keep serving from a corrupted state.

In semver, what does the caret range ^1.2.3 allow?

^1.2.3 permits compatible minor and patch updates (>=1.2.3 <2.0.0); ~1.2.3 allows only patch updates (>=1.2.3 <1.3.0). package-lock.json pins the exact resolved versions.