Skip to content

1. Node.js Core

The fundamentals every Node interview opens with.


1. What Is Node.js?

A runtime, not a language and not a framework. It bundles:

  • V8 — Google's JavaScript engine, compiles JS to machine code
  • libuv — C library providing the event loop and async I/O
  • Node APIsfs, http, crypto, path, stream, etc.

Node vs Browser JavaScript

BrowserNode
Global objectwindowglobal / globalThis
DOMYesNo
File systemNoYes
ModulesESMCommonJS + ESM
requireNoYes (CJS)
APIsfetch, localStoragefs, process, Buffer

2. The Event Loop (Most Asked Question)

Node is single-threaded for your JavaScript, but I/O is not.

The Flow

  1. Your code runs on the single main thread
  2. When it hits async I/O, Node hands it to libuv — which uses OS async APIs, or a thread pool for filesystem and DNS work
  3. Your code continues; it does not wait
  4. When the I/O finishes, its callback is queued
  5. The event loop picks queued callbacks and runs them on the main thread

The Phases

The loop runs through these in order, repeatedly:

PhaseHandles
timerssetTimeout, setInterval callbacks
pending callbacksDeferred system callbacks (some TCP errors)
idle / prepareInternal
pollRetrieve new I/O events; execute I/O callbacks
checksetImmediate callbacks
close callbackssocket.on("close")

Microtasks Run Between Every Phase

process.nextTick and promise callbacks are not phases. They drain after each phase and after each callback:

  • process.nextTick queue drains first
  • then the promise microtask queue
js
console.log("1");

setTimeout(() => console.log("2"), 0);
setImmediate(() => console.log("3"));

Promise.resolve().then(() => console.log("4"));
process.nextTick(() => console.log("5"));

console.log("6");

Output:

1
6
5   ← nextTick — highest priority microtask
4   ← promise microtask
2   ← timers phase
3   ← check phase

Interview Point

setTimeout(fn, 0) vs setImmediate(fn) at the top level is non-deterministic — it depends on how long the process took to start. Inside an I/O callback, setImmediate always wins, because the loop is already past timers and hits check next. Knowing that distinction is a strong signal.


3. Blocking the Event Loop

The single biggest Node production mistake.

js
// ❌ Blocks EVERY request for the whole loop
app.get("/hash", (req, res) => {
  const hash = crypto.pbkdf2Sync(password, salt, 1_000_000, 64, "sha512");
  res.json({ hash });
});

// ✅ Async version — work happens on the thread pool
app.get("/hash", (req, res) => {
  crypto.pbkdf2(password, salt, 1_000_000, 64, "sha512", (err, hash) => {
    if (err) return res.status(500).json({ error: "Failed" });
    res.json({ hash: hash.toString("hex") });
  });
});

Common Blockers

  • fs.readFileSync, crypto.*Sync, zlib.*Sync
  • JSON.parse on a very large payload
  • A tight loop over a huge array
  • Catastrophic regex backtracking (ReDoS — a real attack vector)

Fixes

SituationFix
CPU work in a requestworker_threads
Heavy background jobA queue (BullMQ) plus a separate worker process
Multi-core utilisationcluster or PM2, one process per core
Long computationChunk it with setImmediate between chunks
js
import { Worker } from "node:worker_threads";

app.get("/report", (req, res) => {
  const worker = new Worker("./report-worker.js", { workerData: req.query });
  worker.on("message", (data) => res.json(data));
  worker.on("error", () => res.status(500).json({ error: "Failed" }));
});

4. Modules — CommonJS vs ESM

CommonJS (the old default)

js
const express = require("express");
module.exports = { getUser };
  • Synchronous loading
  • require can be called conditionally, anywhere
  • __dirname and __filename available

ES Modules (the standard)

js
import express from "express";
export { getUser };
export default router;

Enable with "type": "module" in package.json, or a .mjs extension.

  • Asynchronous, statically analysable — enables tree shaking
  • Imports are hoisted; must be top level (use await import() for dynamic)
  • No __dirname:
js
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));

Comparison

CommonJSESM
Syntaxrequire / module.exportsimport / export
LoadingSynchronousAsynchronous
Tree shakingNoYes
Top-level awaitNoYes
Can import the otherYes (ESM can import CJS)CJS cannot require ESM directly
File extension in importsOptionalRequired

Interview Point

The direction is one-way: ESM can import CommonJS, but CommonJS cannot require an ESM module (only await import()). This is why migrating a large Node codebase to ESM is painful and why plenty of production code is still CommonJS.


5. Streams

Process data in chunks instead of loading it all into memory.

js
// ❌ Loads a 2 GB file entirely into RAM
const data = await fs.promises.readFile("huge.csv");
res.send(data);

// ✅ Constant memory regardless of file size
fs.createReadStream("huge.csv").pipe(res);

The Four Types

TypeExample
Readablefs.createReadStream, req
Writablefs.createWriteStream, res
DuplexTCP socket
Transformzlib.createGzip, a CSV parser

Piping With Error Handling

js
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

await pipeline(
  fs.createReadStream("input.txt"),
  createGzip(),
  fs.createWriteStream("output.txt.gz")
);

pipeline propagates errors and cleans up every stream. Plain .pipe() chains leak file descriptors on error — knowing that is a strong answer.

Backpressure

If the consumer is slower than the producer, data queues in memory. .pipe() and pipeline handle backpressure automatically by pausing the source. Manual .on("data") handlers do not — that is how you get memory blowups.


6. Buffers

Fixed-length binary data — Node's answer to raw bytes.

js
const buf = Buffer.from("Hello", "utf8");
buf.toString("base64");     // "SGVsbG8="
buf.length;                 // 5 bytes, not characters

Used for file I/O, network packets, crypto and image data.

Never use new Buffer() — deprecated and unsafe. Use Buffer.from() or Buffer.alloc(). Buffer.allocUnsafe() is faster but returns uninitialised memory that may contain old data.


7. process and Environment

js
process.env.NODE_ENV;
process.argv;              // CLI arguments
process.cwd();             // working directory
process.memoryUsage();
process.exit(1);           // avoid — let the process end naturally

process.on("uncaughtException", (err) => {
  logger.fatal(err);
  process.exit(1);         // MUST exit — state is unreliable
});

process.on("unhandledRejection", (reason) => {
  logger.error(reason);
  process.exit(1);
});

Interview Point

After an uncaughtException the process is in an unknown state — you cannot safely continue. Log it and exit; let your process manager restart you. Swallowing it and carrying on is a classic bad answer.

In Node 15+, an unhandled promise rejection terminates the process by default, which was the right change.

Graceful Shutdown

js
const server = app.listen(3000);

const shutdown = async (signal) => {
  console.log(`${signal} received, shutting down`);

  server.close(async () => {          // stop accepting new connections
    await db.disconnect();            // finish in-flight work
    process.exit(0);
  });

  setTimeout(() => process.exit(1), 10_000).unref();   // force after 10s
};

process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));

Required for zero-downtime deploys — without it, a rolling restart drops in-flight requests. Docker and Kubernetes send SIGTERM before SIGKILL.


8. Cluster and Scaling

Node uses one CPU core. cluster forks one process per core sharing a port.

js
import cluster from "node:cluster";
import os from "node:os";

if (cluster.isPrimary) {
  for (let i = 0; i < os.availableParallelism(); i++) cluster.fork();

  cluster.on("exit", (worker) => {
    console.log(`Worker ${worker.process.pid} died, restarting`);
    cluster.fork();
  });
} else {
  app.listen(3000);
}

In practice most teams use PM2 or let the container orchestrator run N replicas instead.

Interview Point

Clustering means your app must be stateless. No in-memory sessions, no in-memory cache assumed shared, no in-memory rate-limit counters — each worker has its own. Move that state to Redis. This is the follow-up interviewers are fishing for.


9. node: Built-in Modules

ModuleUse
fs / fs/promisesFilesystem
pathCross-platform paths — always use it, never string concatenation
http / httpsServers and clients
cryptoHashing, encryption, random values
streamChunked data
eventsEventEmitter
osCPU count, memory, platform
urlParsing URLs
worker_threadsCPU-bound parallelism
child_processRun external commands

Prefix with node:import fs from "node:fs" — so it's unambiguous and cannot be shadowed by a malicious package of the same name.

EventEmitter

js
import { EventEmitter } from "node:events";

class OrderService extends EventEmitter {
  async create(data) {
    const order = await db.order.create(data);
    this.emit("order:created", order);
    return order;
  }
}

orders.on("order:created", (order) => sendConfirmationEmail(order));

Add emitter.setMaxListeners(n) or you'll see a memory-leak warning after 10 listeners on one event — deliberately, to catch accidental listener accumulation.


10. async/await Patterns

js
// Sequential — 3× the latency
const user = await getUser(id);
const posts = await getPosts(id);
const comments = await getComments(id);

// Parallel — as slow as the slowest
const [user, posts, comments] = await Promise.all([
  getUser(id),
  getPosts(id),
  getComments(id),
]);

Promise.all vs allSettled vs race vs any

Resolves whenRejects when
allAll succeedAny rejects — you lose the other results
allSettledAll settleNever
raceFirst settlesFirst settles with a rejection
anyFirst succeedsAll reject
js
// One failing service shouldn't kill the whole dashboard
const results = await Promise.allSettled([getStats(), getFeed(), getAlerts()]);

const stats = results[0].status === "fulfilled" ? results[0].value : null;

Sequential Loop Trap

js
// ❌ .forEach does not await — everything fires at once, errors vanish
users.forEach(async (u) => await sendEmail(u));

// ✅ Sequential
for (const u of users) await sendEmail(u);

// ✅ Parallel
await Promise.all(users.map((u) => sendEmail(u)));

// ✅ Parallel with a concurrency limit — the production answer
import pLimit from "p-limit";
const limit = pLimit(5);
await Promise.all(users.map((u) => limit(() => sendEmail(u))));

Say the last one. Unbounded Promise.all over 10,000 items opens 10,000 connections and takes down your database. The concurrency limit is what separates a working answer from a production answer.


11. Timeouts and Cancellation

js
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch(url, { signal: controller.signal });
  return await res.json();
} catch (err) {
  if (err.name === "AbortError") throw new Error("Upstream timed out");
  throw err;
} finally {
  clearTimeout(timeout);
}

An outbound call with no timeout is an availability bug — a slow dependency holds your connections open until you run out.

AbortSignal.timeout(5000) is a shorter built-in equivalent in Node 18+.

© 2025 DDocs · Dipak's Documentation Guide