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:
- timers —
setTimeout/setIntervalcallbacks. - pending callbacks — deferred I/O callbacks.
- poll — retrieve new I/O events; execute their callbacks.
- check —
setImmediatecallbacks. - close —
closeevents (e.g. a socket closing).
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:
| Aspect | CommonJS / ES Modules |
|---|---|
| Syntax | require() / module.exports — import / export |
| Loading | Synchronous require / asynchronous & statically analyzable |
| Default in | .cjs (or no type field) / .mjs (or "type": "module") |
| Top-level await | No / Yes |
"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:
The original Node convention: fn(args, (err, result) => ...) — the first argument is always the error. Nesting many of them creates "callback hell".
An object representing a future value — .then() / .catch(), chainable and composable. Promise.all / allSettled / race coordinate concurrency.
Syntactic sugar over Promises — write asynchronous code that reads sequentially, with normal try/catch for errors. The modern default.
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")
);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.
"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/await —
try/catch. - Callbacks — check the error-first argument every time.
- Promises —
.catch(); an unhandled rejection emitsprocess.on("unhandledRejection"). - EventEmitters — listen for the
"error"event.
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:
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.
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.
spawn / exec / fork to run separate programs or Node scripts and communicate over IPC or streams.
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 globalfetch).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.
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.