Menu
CoddyTech

Node.js Interview Questions and Answers

Node.js questions from process.nextTick versus promises to streams, cluster and graceful shutdown. Runnable examples were checked to print the same on Node.js 24; keep the JavaScript cheat sheet open for the language.

91 questions11 output quizzesRunnable code checked on Node.js 24By Kevin Spektor, Co-founder & CTO

Node.js interview questions for freshers

What a first backend round checks: what Node.js is, non-blocking I/O, npm and the built-in globals.

What is Node.js?

Fresherbasics

Node.js is a JavaScript runtime: it runs JavaScript outside the browser on Google's V8 engine, with libuv for the event loop and asynchronous I/O. It is not a framework; Express runs on it. It adds file system, network and process access, and has no DOM. It suits servers because I/O does not block. More in the Node.js runtime.

What is the REPL in Node.js?

Fresherbasics

The REPL (Read, Eval, Print, Loop) is the interactive shell you get by running node with no file. Use it to try an API without creating a file.

$ node
> [1, 2, 3].map((n) => n * 2)
[ 2, 4, 6 ]
> _.length
3
> .exit

_ holds the last result, .editor accepts several lines at once, and top-level await works.

Is Node.js single-threaded?

Fresherevent loop

Your JavaScript runs on one thread, but the process is not single-threaded. libuv keeps a thread pool (4 threads by default) for work the OS cannot do asynchronously, such as most fs calls, dns.lookup and zlib; network sockets use the OS's own async I/O. Results come back as callbacks on the main thread. To run JavaScript in parallel, use worker_threads or cluster.

What is the difference between JavaScript in Node.js and in the browser?

Fresherbasics

The language is the same; the APIs around it differ.

BrowserNode.js
Global objectwindowglobal (both have globalThis)
DOMyesno
File system, processesnofs, child_process
ModulesES modulesCommonJS and ES modules

Both have fetch (Node since 18, stable in 21). In Node.js setTimeout returns a Timeout object, in the browser a number.

What is the difference between blocking and non-blocking code in Node.js?

Fresherasyncfs

Blocking code makes the thread wait until the work is done; non-blocking code starts it and delivers the result later through a callback, promise or event. fs.readFileSync blocks, fs.readFile does not.

JavaScript
const fs = require('node:fs');

fs.writeFileSync('demo.txt', 'hello');

console.log('1. sync read:', fs.readFileSync('demo.txt', 'utf8'));

fs.readFile('demo.txt', 'utf8', (err, data) => {
  console.log('3. async read:', data);
  fs.unlinkSync('demo.txt');
});

console.log('2. after the readFile call');

Sync calls are fine at startup; inside a request handler they stall every other request.

What is npm, and what is the difference between npm and npx?

Freshernpm

npm is the package manager that ships with Node.js: it installs packages into node_modules and records them in package.json. npx runs a package's command line tool, downloading it temporarily if needed, so you need no global installs. See npm basics.

npm init -y              # create package.json
npm install express      # add a dependency
npm install -D vitest    # add a dev dependency
npm run test             # run the "test" script from package.json
npx eslint .             # run a tool without a global install

What do ^ and ~ mean in package.json versions?

Freshernpm

Semver ranges. ~1.4.2 allows patch updates (<1.5.0); ^1.4.2, npm's default, allows minor and patch updates (<2.0.0). The trap: below 1.0.0, ^0.4.2 allows only patches (<0.5.0), because a 0.x minor release may break things.

What is package-lock.json, and when do you use npm ci instead of npm install?

Freshernpm

package-lock.json records the exact version and hash of every installed package, transitive ones included, so installs are reproducible; commit it. npm install may update the lock file. npm ci installs exactly what it says, deletes node_modules first, and fails if it disagrees with package.json. Use npm ci in CI and Docker builds.

What is an error-first callback?

Fresherasyncerrors

The Node.js callback convention: the first argument is an error or null, the rest are results. util.promisify relies on it.

JavaScript
function divide(a, b, callback) {
  setTimeout(() => {
    if (b === 0) return callback(new Error('division by zero'));
    callback(null, a / b);
  }, 0);
}

function print(err, result) {
  if (err) return console.log('error:', err.message);
  console.log('result:', result);
}

divide(10, 2, print);
divide(1, 0, print);

Two rules: return after calling back with an error, and never call the callback twice. More in callbacks.

What is callback hell, and how do you avoid it?

Fresherasync

Callback hell is each async step nested inside the previous callback, so the code drifts right and every level repeats its error check. The fix is async/await: flat code with one try/catch, using util.promisify or Node's promise APIs (fs/promises).

JavaScript
const { promisify } = require('node:util');

function getUser(id, cb) { setTimeout(() => cb(null, { id, name: 'Ada' }), 1); }
function getOrders(user, cb) { setTimeout(() => cb(null, [`${user.name}-1`, `${user.name}-2`]), 1); }
function getInvoice(order, cb) { setTimeout(() => cb(null, `invoice for ${order}`), 1); }

// callback hell: every step nests one level deeper and repeats the error check
getUser(7, (err, user) => {
  if (err) return console.log(err.message);
  getOrders(user, (err, orders) => {
    if (err) return console.log(err.message);
    getInvoice(orders[0], (err, invoice) => {
      if (err) return console.log(err.message);
      console.log('callbacks:', invoice);
      flat();
    });
  });
});

// the same steps, flat
async function flat() {
  try {
    const user = await promisify(getUser)(7);
    const orders = await promisify(getOrders)(user);
    console.log('async/await:', await promisify(getInvoice)(orders[0]));
  } catch (err) {
    console.log(err.message);
  }
}

What is EventEmitter in Node.js?

Fresherevents

EventEmitter from node:events is the publish/subscribe class behind streams, servers and sockets: on registers a listener, emit calls the listeners.

JavaScript
const EventEmitter = require('node:events');

class Order extends EventEmitter {
  pay(amount) {
    this.emit('paid', amount);
  }
}

const order = new Order();
order.on('paid', (amount) => console.log('send receipt for', amount));
order.once('paid', () => console.log('first payment only'));

order.pay(250);
order.pay(100);
console.log('listeners left:', order.listenerCount('paid'));

emit is synchronous: it calls every listener, in order, before it returns. once removes its listener after the first call.

What does this print: emit called between two log pushes?

Fresherevents
JavaScript
const EventEmitter = require('node:events');
const bus = new EventEmitter();
const log = [];

bus.on('save', () => log.push('listener'));

log.push('before');
bus.emit('save');
log.push('after');
console.log(log.join(' '));

Predict the output

emit calls the listeners synchronously, so listener is pushed before emit returns. An emitter is not a queue. To run a listener later, defer inside it, for example with setImmediate.

What are __dirname, __filename, process and global?

Freshermodulesprocess

__dirname and __filename are the directory and file path of the current CommonJS module. process describes the running process (env, argv, exit()). global is the global object, also globalThis. ES modules have no __dirname: use import.meta.dirname (Node 20.11 and later) or derive it:

import { fileURLToPath } from 'node:url';
import path from 'node:path';

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

What does this print: a number assigned to process.env.PORT?

Fresherprocess
JavaScript
process.env.PORT = 3000;
const port = process.env.PORT;
console.log(typeof port, port === 3000, port + 1);

Predict the output

Every process.env value is a string; Node converts what you assign. So port === 3000 is false and port + 1 concatenates. Parse configuration once at startup: Number(process.env.PORT ?? 3000).

What is NODE_ENV, and why set it to production?

Fresherprocess

NODE_ENV is a convention that libraries read to choose development or production behavior; Node.js itself ignores it. With production, Express caches view templates and hides stack traces, and npm install skips devDependencies. Read it once into a config object instead of branching on it across the codebase.

How do you read command line arguments in Node.js?

Fresherprocess

process.argv holds the node binary, the script, then your arguments from index 2. For flags, use the built-in util.parseArgs (Node 18.3 and later).

// node greet.js --name Ada --loud
const { parseArgs } = require('node:util');

const { values } = parseArgs({
  args: process.argv.slice(2),
  options: {
    name: { type: 'string', default: 'world' },
    loud: { type: 'boolean' },
  },
});
const text = `hello ${values.name}`;
console.log(values.loud ? text.toUpperCase() : text);
// HELLO ADA

When should you use Node.js, and when should you not?

Fresherbasics

Use Node.js for I/O bound services: APIs, chat and WebSockets, proxies, streaming, CLI tools. Waiting costs almost nothing, so one thread holds thousands of connections. Avoid it for CPU bound work such as video encoding or ML training: a long computation blocks the event loop and every request waits, unless you move it to worker_threads or another service.

Node.js event loop and async interview questions

How Node.js runs timers, I/O callbacks and promises on one thread, and the ordering questions built on that.

Explain the Node.js event loop and its phases.

Experiencedevent loop

The event loop is libuv's loop that runs ready callbacks on the main thread. Each turn has six phases, each with its own queue:

  1. timers: due setTimeout and setInterval callbacks
  2. pending callbacks: some deferred system I/O callbacks
  3. idle, prepare: internal
  4. poll: wait for and run I/O callbacks
  5. check: setImmediate callbacks
  6. close callbacks: 'close' events

After every callback (timers and setImmediate too, since Node 11), Node drains process.nextTick, then promise microtasks. See the event loop.

What does this print: a 0 ms timer, queueMicrotask and a promise?

Fresherevent loop
JavaScript
const log = [];
setTimeout(() => log.push('timeout'), 0);
queueMicrotask(() => log.push('microtask'));
Promise.resolve().then(() => log.push('promise'));
log.push('sync');
setTimeout(() => console.log(log.join(' ')), 5);

Predict the output

Sync code runs first, then microtasks in queueing order: queueMicrotask was called before .then. The loop reaches the timers phase only when the microtask queue is empty, even for a 0 ms timer.

What is the difference between process.nextTick, setImmediate and setTimeout(fn, 0)?

Experiencedevent loop

process.nextTick runs right after the current operation, before promise callbacks (in CommonJS; see the ES module question). setImmediate runs in the check phase, after I/O polling. setTimeout(fn, 0) runs in the timers phase after at least 1 ms.

// Node.js, CommonJS file
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');

Node prints sync, nextTick, promise, then timeout and immediate in either order. The names are backwards: nextTick is the immediate one, and a recursive nextTick can starve I/O.

Does setTimeout(fn, 0) or setImmediate run first?

Experiencedevent loop

From the main module it is not guaranteed: it depends on whether 1 ms has passed when the loop reaches the timers phase. Inside an I/O callback setImmediate always runs first, because check comes right after poll.

const fs = require('node:fs');

fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
});
// Node.js always prints:
// immediate
// timeout

setImmediate is a Node.js API, not a web standard, so this order is a Node.js rule; see setImmediate vs setTimeout.

Why does process.nextTick run after a promise callback in an ES module but before it in CommonJS?

Seniorevent loopmodules

Because an ES module's top-level code runs inside a promise job. When a CommonJS file ends, Node drains nextTick first, then microtasks. An ES module body ends while microtasks are already draining, so its promise callbacks run first.

process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
FileNode.js prints
order.cjssync, nextTick, promise
order.mjssync, promise, nextTick

The process.nextTick docs show both orders. Never depend on this order; make it explicit with await.

What happens when you block the event loop?

Experiencedevent loopperformance

Everything waits: timers fire late, requests queue up, health checks time out.

JavaScript
const start = Date.now();

setTimeout(() => {
  const waited = Date.now() - start;
  console.log('asked for 10 ms, waited at least 200:', waited >= 200);
}, 10);

// 200 ms of synchronous work, like a huge JSON.parse or a sync crypto call
while (Date.now() - start < 200) {}
console.log('busy loop done');

Common causes: JSON.parse on huge payloads, sync fs or crypto in a handler, a regex with catastrophic backtracking. Measure with perf_hooks.monitorEventLoopDelay(); fix by chunking with setImmediate or a worker thread.

What is the libuv thread pool, and which operations use it?

Seniorevent loopperformance

A pool of threads (4 by default) that libuv uses for work the OS cannot do asynchronously. Your JavaScript never runs there.

Uses the poolDoes not
fs (not fs.watch)sockets, HTTP
dns.lookupdns.resolve*
async crypto.pbkdf2, scrypttimers
async zlibchild process pipes

The trap: with 4 threads, 8 concurrent pbkdf2 calls take about twice as long as 4. Raise it with the UV_THREADPOOL_SIZE environment variable before Node starts (maximum 1024).

How can one Node.js thread handle thousands of concurrent connections?

Experiencedevent loopscaling

A waiting connection costs almost nothing: Node registers each socket with the OS (epoll, kqueue, IOCP) and runs code only when data arrives, where a thread-per-request server parks a thread per client. The limit is CPU per request: at 5 ms of JavaScript each, one process tops out near 200 requests per second.

What does this print: await inside a forEach callback?

Experiencedasync
JavaScript
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function run() {
  const done = [];
  [30, 10, 20].forEach(async (ms) => {
    await sleep(ms);
    done.push(ms);
  });
  const afterLoop = done.length;
  await sleep(50);
  console.log(afterLoop, done.join(','));
}
run();

Predict the output

forEach ignores the promises its callback returns, so right after the loop nothing is done, and the sleeps finish in time order. Use await Promise.all(items.map(...)) (parallel) or for...of with await (one at a time). See async and await.

How do you turn a callback API into a promise in Node.js?

Fresherasync

Use util.promisify for any function with an error-first callback, or the promise APIs Node ships: fs/promises, timers/promises, stream/promises.

JavaScript
const util = require('node:util');

function legacyDouble(x, callback) {
  setTimeout(() => {
    if (x < 0) return callback(new Error('negative input'));
    callback(null, x * 2);
  }, 1);
}

const double = util.promisify(legacyDouble);

async function main() {
  console.log(await double(21));
  try {
    await double(-1);
  } catch (err) {
    console.log('caught:', err.message);
  }
}
main();

What is the difference between Promise.all, Promise.allSettled, Promise.race and Promise.any?

Experiencedasync

They differ in when they settle:

MethodFulfillsRejects
allall fulfill, with an arrayat the first rejection
allSettledall settle, with a status eachnever
racefirst to settle, if it fulfilledfirst to settle, if it rejected
anyfirst to fulfillall reject (AggregateError)
JavaScript
const ok = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (ms, msg) => new Promise((_, reject) => setTimeout(() => reject(new Error(msg)), ms));

async function main() {
  try {
    await Promise.all([ok(20, 'a'), fail(10, 'b failed'), ok(30, 'c')]);
  } catch (err) {
    console.log('all:', err.message);
  }
  const settled = await Promise.allSettled([ok(20, 'a'), fail(10, 'b failed')]);
  console.log('allSettled:', settled.map((r) => r.status).join(', '));
  console.log('race:', await Promise.race([ok(20, 'slow'), ok(5, 'fast')]));
  console.log('any:', await Promise.any([fail(5, 'x'), ok(20, 'first success')]));
}
main();

When Promise.all rejects, the other operations keep running; nothing is cancelled.

Node.js modules interview questions: CommonJS vs ES modules

How require and import find, load and cache code, and where the two systems differ.

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

Freshermodules

CommonJS (require) is Node's original synchronous system; ES modules (import) are the standard, linked before the code runs.

CommonJSES modules
Top-level awaitnoyes
__dirnameyesno, import.meta.dirname
Extension in relative pathsoptionalrequired
Imported valuesread once at requirelive bindings

Since Node 22.12 (and 20.19), CommonJS can require() an ES module without top-level await. See CommonJS vs ES modules.

What does this print: reassigning exports vs adding to module.exports?

Freshermodules
JavaScript
exports.a = 1;
exports = { b: 2 };
module.exports.c = 3;
console.log(JSON.stringify(module.exports));

Predict the output

exports starts as a reference to module.exports, which is what require returns. exports.a = 1 adds to that object; exports = { b: 2 } only rebinds the local variable. To export one function or class, assign module.exports.

How does require work, and where do exports, require and __dirname come from?

Experiencedmodules

Node wraps each CommonJS file in a function called with exports, require, module, __filename and __dirname, so those names are local to each file.

JavaScript
// a CommonJS file: these names come from the module wrapper
console.log(arguments.length);
console.log(typeof exports, typeof require, typeof module);
console.log(typeof __filename, typeof __dirname);
console.log(require.main === module);

require resolves the path, runs the file once, caches module.exports and returns it. require.main === module means the file was run directly.

Are Node.js modules cached? What does that mean for singletons?

Experiencedmodules

Yes. The first require runs the file and caches it by resolved path; later calls return the same module.exports. So an exported database pool is a singleton per process.

// counter.js
let count = 0;
module.exports = { next: () => ++count };

// app.js
const a = require('./counter');
const b = require('./counter');
console.log(a === b, a.next(), b.next()); // true 1 2

Traps: one package installed in two node_modules folders gives two instances, and each cluster worker or worker thread has its own cache.

How does Node.js resolve the path in require('something')?

Experiencedmodules

In this order:

  1. Core modules: fs or node:fs (node:test exists only with the prefix).
  2. A path starting with ./, ../ or /: the exact name, then .js, .json, .node, then a folder's main or index.js.
  3. A package: node_modules in this folder, then in each parent up to the root, then the package's exports field (or main).

ES modules do not try extensions, and exports blocks paths it does not list.

What happens with circular dependencies in Node.js?

Seniormodules

Node hands out a partly filled module. When a.js and b.js require each other, b gets a's module.exports as it is at that moment.

// a.js
exports.loaded = false;
const b = require('./b');
console.log('in a, b.loaded =', b.loaded);
exports.loaded = true;

// b.js
const a = require('./a');
console.log('in b, a.loaded =', a.loaded);
exports.loaded = true;

// node a.js prints:
// in b, a.loaded = false
// in a, b.loaded = true

In ES modules, reading an imported let, const or class too early throws a ReferenceError. Fix the design: move the shared code into a third module, or require lazily inside a function.

What is dynamic import() and when do you use it in Node.js?

Experiencedmodules

import() loads a module at runtime and returns a promise for it, in CommonJS and ES modules, with any expression as the specifier.

JavaScript
async function load(name) {
  // the module is chosen at runtime, which a static import cannot do
  return import(name);
}

load('node:path').then((path) => {
  console.log(path.join('reports', '2026', 'march.csv'));
  console.log(path.extname('archive.tar.gz'));
});

Use it to load an ES-only package from CommonJS, plugins named in config, or a heavy module only when needed.

How does Node.js decide whether a file is CommonJS or an ES module?

Freshermodules

.mjs is always an ES module, .cjs always CommonJS. A .js file follows the nearest package.json "type": "module" means ESM, "commonjs" CommonJS. With no type field (or no package.json), Node 22.7 and later try CommonJS first and rerun a file that uses import or export as ESM (syntax detection); that costs a second parse and warns when a package.json exists, so set "type" explicitly. See ES modules.

Node.js streams and buffers interview questions

Buffers for binary data, streams for data that does not fit in memory, and backpressure.

What is a Buffer in Node.js?

Fresherbuffers

A Buffer is a fixed-length chunk of raw bytes (a Uint8Array subclass) for binary data. Strings are text, buffers are bytes, and an encoding converts between them.

JavaScript
const buf = Buffer.from('Hi Node', 'utf8');

console.log(buf.length);
console.log(buf.toString('hex'));
console.log(buf.toString('base64'));
console.log(Buffer.from('SGkgTm9kZQ==', 'base64').toString('utf8'));
console.log(buf[0], String.fromCharCode(buf[0]));

new Buffer() is deprecated; use Buffer.from or Buffer.alloc.

What does this print: string length vs byte length of héllo?

Fresherbuffers
JavaScript
const word = 'héllo';
console.log(word.length, Buffer.byteLength(word), Buffer.from(word).length);

Predict the output

length counts UTF-16 code units; a buffer counts bytes, and UTF-8 é takes two. It matters for Content-Length headers and byte limits.

What does this print: out-of-range numbers passed to Buffer.from?

Experiencedbuffers
JavaScript
const buf = Buffer.from([65, 256, 257, -1, 3.9]);
console.log(buf.join(','));

Predict the output

Each element is one unsigned byte: values are truncated, then wrapped modulo 256, so -1 becomes 255 and 3.9 becomes 3, with no error. buf.writeUInt8(-1) throws a RangeError instead.

What does this print: writing through a subarray and a copy?

Experiencedbuffers
JavaScript
const original = Buffer.from('cat');
const part = original.subarray(0, 1);
const copy = Buffer.from(original);

part[0] = 'b'.charCodeAt(0);
console.log(original.toString(), copy.toString());

Predict the output

subarray returns a view over the same memory, so writing through part changes original; Buffer.from(buffer) copies. A tiny subarray also keeps the whole large buffer alive.

What is the difference between Buffer.alloc and Buffer.allocUnsafe?

Experiencedbufferssecurity

Buffer.alloc(n) returns n zeroed bytes. Buffer.allocUnsafe(n) skips the zeroing, so it is faster, but may hold old data, including secrets from other requests. Use it only when you overwrite every byte at once.

What are streams in Node.js, and what are the four types?

Fresherstreams

A stream processes data piece by piece instead of loading it all into memory. The four types: Readable (fs.createReadStream, an HTTP request), Writable (an HTTP response), Duplex (a TCP socket) and Transform, a Duplex that changes the data (zlib.createGzip()).

JavaScript
const { Readable, Transform } = require('node:stream');
const { pipeline } = require('node:stream/promises');

let calls = 0;
const upper = new Transform({
  transform(chunk, encoding, callback) {
    calls++;
    callback(null, chunk.toString().toUpperCase());
  },
});

async function main() {
  const out = [];
  await pipeline(
    Readable.from(['stream ', 'data ', 'in chunks']),
    upper,
    async function (source) {
      for await (const chunk of source) out.push(chunk.toString());
    },
  );
  console.log(out.join(''));
  console.log('transform calls:', calls);
}
main();

Readable streams are async iterables, so for await is the simplest way to consume one.

Why use streams to serve a large file instead of fs.readFile?

Experiencedstreams

Memory and time to first byte. readFile loads the whole file first, so a 2 GB file needs 2 GB of RAM per download. A stream sends 64 KiB chunks as they are read, so memory stays flat.

const fs = require('node:fs');
const http = require('node:http');
const { pipeline } = require('node:stream/promises');

http.createServer(async (req, res) => {
  res.setHeader('Content-Type', 'video/mp4');
  try {
    await pipeline(fs.createReadStream('movie.mp4'), res);
  } catch (err) {
    console.error('download failed:', err.code);
  }
}).listen(3000);

pipeline also closes the file stream if the client disconnects.

What is backpressure in streams, and how does Node.js handle it?

Seniorstreams

Backpressure is a producer outrunning its consumer. A writable returns false from write() once its buffer passes highWaterMark (64 KiB by default in Node 22 and later); the producer should then wait for 'drain'.

function writeLines(stream, lines, done) {
  let i = 0;
  function writeSome() {
    while (i < lines.length) {
      const ok = stream.write(lines[i++] + '\n');
      if (!ok) return stream.once('drain', writeSome); // buffer full, wait
    }
    done();
  }
  writeSome();
}

pipe() and pipeline() do this for you. The bug to spot: a write() loop that ignores the return value runs out of memory in production.

What is the difference between pipe() and pipeline()?

Experiencedstreamserrors

Both connect streams with backpressure, but pipe() does not forward errors: a failed source leaves the destination open, and an unhandled 'error' crashes the process. pipeline() destroys every stream when one fails and gives you one place to catch.

// leaks the file stream on failure, and a gzip error crashes the process
source.pipe(zlib.createGzip()).pipe(fs.createWriteStream('out.gz'));

// one try/catch, everything cleaned up
await pipeline(source, zlib.createGzip(), fs.createWriteStream('out.gz'));

How do you read a large file line by line in Node.js?

Fresherstreamsfs

Use readline on a read stream: it is an async iterable and holds only the current chunk.

JavaScript
const fs = require('node:fs');
const readline = require('node:readline');

fs.writeFileSync('access.log', 'GET /home 200\nGET /cart 500\nPOST /pay 200\n');

async function countErrors(file) {
  const rl = readline.createInterface({
    input: fs.createReadStream(file),
    crlfDelay: Infinity, // treat \r\n as a single line break
  });
  let lines = 0;
  let errors = 0;
  for await (const line of rl) {
    lines++;
    if (line.endsWith(' 500')) errors++;
  }
  return { lines, errors };
}

countErrors('access.log').then(({ lines, errors }) => {
  console.log(`${errors} of ${lines} requests failed`);
  fs.unlinkSync('access.log');
});

It reads a 10 GB log in kilobytes of memory, unlike readFile plus split('\n').

Express.js interview questions (Node.js basics)

Express examples are static because this page has no npm packages; the last question uses http and runs.

What is Express.js, and why use it instead of the http module?

Fresherexpress

Express is a minimal web framework on Node's http module that adds routing, middleware, body parsing and helpers such as res.json.

const express = require('express');
const app = express();

app.use(express.json()); // parse JSON bodies into req.body

app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id });
});

app.listen(3000, () => console.log('listening on 3000'));

Alternatives: Fastify (faster, schema validation), NestJS (dependency injection, on Express or Fastify) and Koa.

What is middleware in Express?

Fresherexpress

A function (req, res, next) that runs during a request, in registration order. It can change req and res, end the response, or call next(). Logging, auth and body parsing are middleware.

function requestTimer(req, res, next) {
  const start = Date.now();
  res.on('finish', () => {
    console.log(req.method, req.url, res.statusCode, Date.now() - start, 'ms');
  });
  next();
}

function requireAuth(req, res, next) {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'missing token' });
  }
  next();
}

app.use(requestTimer);                        // every route
app.get('/orders', requireAuth, listOrders);  // one route

Classic bugs: neither calling next() nor responding hangs the request; calling next() after responding lets a later handler respond again, which throws "Cannot set headers after they are sent to the client".

What is the difference between req.params, req.query and req.body?

Fresherexpress

req.params holds route segments (/users/:id), req.query the query string (?page=2), req.body the parsed body. Params and query values are strings, so convert and validate them, and req.body is undefined without a parser such as express.json().

How do you handle errors in Express?

Fresherexpresserrors

Add a middleware with four parameters (err, req, res, next) after all routes; errors passed to next(err) or thrown in a handler go there.

app.get('/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id); // may reject
  if (!user) {
    const err = new Error('user not found');
    err.status = 404;
    throw err;
  }
  res.json(user);
});

// last; the four parameters are how Express recognizes it
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status ?? 500).json({ error: err.status ? err.message : 'internal error' });
});

Express 5 forwards a rejected async handler there too. Express 4 does not: the request hangs and, on Node 15 and later, the process crashes, so catch and call next(err).

What is express.Router and how do you structure routes?

Fresherexpress

express.Router() is a mini app with its own routes and middleware, mounted under a prefix to split an API into files.

// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', listUsers);     // GET /api/users
router.get('/:id', getUser);    // GET /api/users/42
router.post('/', createUser);   // POST /api/users

module.exports = router;

// app.js
app.use('/api/users', require('./routes/users'));

Keep routes thin: they parse input and call a service, which holds the business logic, so it stays testable without HTTP.

What is the difference between app.use and app.get?

Fresherexpress

app.get matches only GET (and HEAD) on the exact path. app.use matches every method and every path under the prefix, and strips the prefix from req.url. Mount routers and middleware with use, endpoints with get or post.

app.use('/api', apiLogger);       // GET /api/users, POST /api/orders/7, ...
app.get('/api/users', listUsers); // only GET /api/users

What is CORS, and how do you enable it in a Node.js API?

Experiencedexpresssecurity

CORS is a browser rule: a page may read a response from another origin only if the API's headers allow it. Only browsers enforce it, so it does not protect your API. For JSON bodies, custom headers or PUT, the browser first sends an OPTIONS preflight.

const cors = require('cors');

app.use(cors({
  origin: ['https://app.example.com'], // never '*' when sending cookies
  credentials: true,
}));

How do you create an HTTP server without Express?

Fresherhttp

With node:http: createServer takes a (req, res) handler for every request, and routing and parsing are yours.

JavaScript
const http = require('node:http');

const server = http.createServer((req, res) => {
  const url = new URL(req.url, 'http://localhost');
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ path: url.pathname, x: url.searchParams.get('x') }));
});

server.listen(0, () => {
  const { port } = server.address();
  http.get(`http://127.0.0.1:${port}/hello?x=1`, (res) => {
    let body = '';
    res.setEncoding('utf8');
    res.on('data', (chunk) => (body += chunk));
    res.on('end', () => {
      console.log(res.statusCode, res.headers['content-type'], body);
      server.close();
    });
  });
});

Port 0 asks the OS for any free port, the usual trick in tests.

Node.js error handling interview questions

Where an error goes in callbacks, promises, emitters and the process, and what a service should do with it.

What does this print: try/catch around a callback that fails later?

Freshererrorsasync
JavaScript
const log = [];

function loadLater(callback) {
  setTimeout(() => callback(new Error('late')), 0);
}

try {
  loadLater((err) => {
    log.push('callback: ' + err.message);
    console.log(log.join(', '));
  });
  log.push('try done');
} catch (err) {
  log.push('caught');
}

Predict the output

try/catch covers only code that runs synchronously inside it. When the timer fires, the try has finished, so the callback's error never reaches the catch. That is why callback APIs pass errors as an argument. With promises, await brings the error back into the try.

How do you handle errors with async/await in Node.js?

Freshererrorsasync

Wrap the await in try/catch: a rejected promise throws at the await. Put cleanup in finally.

JavaScript
async function fetchUser(id) {
  if (id <= 0) throw new Error(`invalid id ${id}`);
  return { id, name: 'Ada' };
}

async function handler(id) {
  try {
    const user = await fetchUser(id);
    return `found ${user.name}`;
  } catch (err) {
    return `failed: ${err.message}`;
  } finally {
    console.log(`request ${id} finished`);
  }
}

async function main() {
  console.log(await handler(7));
  console.log(await handler(-1));
}
main();

finally runs before the promise settles, so request 7 finished prints before found Ada. Catch only where you can act; let the rest reach a central handler. See error handling in async code.

What does this print: return vs return await inside try/catch?

Experiencederrorsasync
JavaScript
async function fail() {
  throw new Error('boom');
}

async function withoutAwait() {
  try {
    return fail();
  } catch {
    return 'caught';
  }
}

async function withAwait() {
  try {
    return await fail();
  } catch {
    return 'caught';
  }
}

async function main() {
  const a = await withoutAwait().catch((e) => 'rejected: ' + e.message);
  const b = await withAwait();
  console.log(a, '|', b);
}
main();

Predict the output

return fail() returns the rejected promise without waiting, so the function leaves the try before it rejects. return await rejects inside the try, so it is caught. Inside try/catch, always return await.

What happens to an unhandled promise rejection in Node.js?

Experiencederrors

Since Node 15 the process crashes with exit code 1, like an uncaught exception; before 15 it only warned.

process.on('unhandledRejection', (reason) => {
  console.error('unhandled rejection:', reason);
  process.exitCode = 1;
  // log, flush, then shut down; do not carry on as if nothing happened
});

async function save() {
  throw new Error('db down');
}
save(); // no await, no .catch(): this is the bug

The handler is for logging and shutdown. The fix is at the call site: await the promise or add .catch().

Should you use process.on('uncaughtException') to keep a server running?

Seniorerrorsproduction

No. Log and exit: after an uncaught exception the state is unknown (a half-done transaction, a held lock), and Node's documentation says resuming is unsafe.

process.on('uncaughtException', (err) => {
  console.error('fatal:', err);
  process.exit(1); // the process manager starts a clean instance
});

Let a supervisor (Kubernetes, systemd, PM2) restart it, and run several instances.

What does this print: emitting 'error' with no listener?

Experiencederrorsevents
JavaScript
const EventEmitter = require('node:events');
const job = new EventEmitter();
let result;

try {
  job.emit('error', new Error('disk full'));
  result = 'emitted';
} catch (err) {
  result = 'thrown: ' + err.message;
}
console.log(result);

Predict the output

With no 'error' listener, emit('error') throws; other events with no listener are ignored. A missing stream.on('error', ...) is a common production crash, so attach one or use pipeline.

What is the difference between operational errors and programmer errors?

Seniorerrors

Operational errors are expected failures of a correct program: the database is down, input is invalid, a request timed out. Handle them: retry, return a 4xx or 5xx. Programmer errors are bugs, such as reading a property of undefined; log, crash, restart and fix the code. Branch on err.code (ENOENT, ECONNRESET), never on messages.

How do you create custom error classes in Node.js?

Experiencederrors

Extend Error, set name, add fields such as status, and pass the original error as cause (Node 16.9 and later).

JavaScript
class AppError extends Error {
  constructor(message, { status = 500, cause } = {}) {
    super(message, { cause });
    this.name = this.constructor.name;
    this.status = status;
  }
}

class NotFoundError extends AppError {
  constructor(what, options) {
    super(`${what} not found`, { status: 404, ...options });
  }
}

try {
  try {
    JSON.parse('{bad json');
  } catch (err) {
    throw new NotFoundError('config', { cause: err });
  }
} catch (err) {
  console.log(err.name, err.status, err.message);
  console.log(err instanceof AppError, err.cause instanceof SyntaxError);
}

Handlers branch on instanceof or status, not on message text. See error types.

How do you add a timeout to an async operation?

Experiencedasyncerrors

Race the operation against a rejecting timer, and clear the timer afterwards.

JavaScript
function withTimeout(promise, ms) {
  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error(`timed out after ${ms} ms`)), ms);
  });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

const sleep = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

async function main() {
  console.log(await withTimeout(sleep(10, 'fast result'), 100));
  try {
    await withTimeout(sleep(200, 'slow result'), 50);
  } catch (err) {
    console.log(err.message);
  }
}
main();

Promise.race stops the waiting, not the work. Where cancellation is supported, pass a signal: fetch(url, { signal: AbortSignal.timeout(5000) }).

Node.js performance and scaling: cluster and worker threads

Using every CPU core and keeping CPU-heavy work off the event loop.

What is the cluster module and how does it work?

Experiencedscaling

cluster forks copies of the server, usually one per core, sharing one port; the primary hands connections to workers round-robin (except on Windows).

const cluster = require('node:cluster');
const http = require('node:http');
const os = require('node:os');

if (cluster.isPrimary) {
  for (let i = 0; i < os.availableParallelism(); i++) cluster.fork();
  cluster.on('exit', (worker, code) => {
    console.log(`worker ${worker.process.pid} exited with ${code}, starting a new one`);
    cluster.fork();
  });
} else {
  http.createServer((req, res) => res.end(`handled by ${process.pid}\n`)).listen(3000);
}

Workers share no memory, so sessions and caches move to Redis. In containers the usual choice is one process per container instead.

What are worker threads in Node.js, and when do you use them?

Experiencedscaling

worker_threads runs JavaScript in parallel threads in one process, each with its own V8 isolate and event loop, talking by messages or a SharedArrayBuffer. Use them for CPU-heavy work.

const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');

function fib(n) {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

if (isMainThread) {
  const worker = new Worker(__filename, { workerData: 35 });
  worker.on('message', (result) => console.log('fib(35) =', result));
  worker.on('error', (err) => console.error(err));
  console.log('main thread is still free');
} else {
  parentPort.postMessage(fib(workerData));
}
// Node.js prints:
// main thread is still free
// fib(35) = 9227465

Not for I/O, which is already async. Keep a fixed pool (piscina) rather than one worker per request.

What is the difference between cluster, worker_threads and child_process?

Seniorscaling
clusterworker_threadschild_process
Unitprocessthreadprocess
Memoryseparateseparate heaps, optional SharedArrayBufferseparate
Use forall cores for HTTPCPU-heavy JavaScriptother programs (ffmpeg, git)

A thread is cheaper than a process, but a native crash in a worker kills the whole process, while a crashed child leaves the parent running.

What is the difference between spawn, exec, execFile and fork?

Experiencedscalingsecurity
FunctionShellOutputUse for
spawnno (by default)streamslong processes, large output
execyesbuffered (1 MB default)short shell commands
execFilenobuffereda program with arguments
forknostdio plus IPCa Node.js script you message

exec('convert ' + userFile) is command injection. Pass user input as an argument array to execFile or spawn.

How do you handle a CPU-intensive task in a Node.js API?

Experiencedperformance

Get it off the event loop: a job queue (BullMQ, SQS) when the client can wait, replying 202 with a job id; a worker thread pool when the response needs the result; or chunking with setImmediate as a last resort. The wrong answer is async: wrapping a synchronous loop in an async function does not move it to another thread.

How do you find out why a Node.js service is slow?

Seniorperformance

Measure, then tell the kinds of slow apart:

  • Event loop blocked (every endpoint slow, one core at 100%): CPU profile with node --cpu-prof or --inspect, read the flame graph.
  • Waiting on I/O (CPU low): time database and downstream calls, check pool limits, including the libuv pool.
  • Memory pressure (RSS rising, latency spikes): GC pauses; use heap snapshots.

perf_hooks.monitorEventLoopDelay() exported as a metric shows a blocked loop.

What is the V8 heap limit in Node.js and how do you change it?

Seniorperformancememory

V8 caps the old-generation heap, and past it the process dies with "JavaScript heap out of memory". Since Node 12 the default depends on available memory; read it from v8.getHeapStatistics().heap_size_limit and change it with --max-old-space-size=4096 (MiB), below the container limit. If usage grows without end it is a leak, and a bigger heap only delays the crash.

How would you add caching to a Node.js API?

Seniorperformancecaching

Pick by scope: an in-process LRU Map for small hot data (each process has its own copy), Redis for anything shared across instances, and HTTP caching (Cache-Control, a CDN) for public GET responses. Two traps: an unbounded Map cache is a memory leak, so cap it and expire entries; and when a hot key expires, many requests recompute it at once, so cache the in-flight promise.

Node.js interview questions for experienced developers (3 to 10 years)

Production behavior and design: shutdown, request context, leaks, security, testing and scaling out.

How do you shut down a Node.js server gracefully?

Seniorproduction

On SIGTERM, stop accepting connections, let in-flight requests finish, close database pools, then exit, with a hard timeout in case something hangs.

const server = app.listen(3000);

function shutdown(signal) {
  console.log(`${signal} received, closing`);
  server.close(async () => {   // stop accepting, wait for open requests
    await db.end();            // close the database pool
    process.exit(0);
  });
  setTimeout(() => process.exit(1), 10_000).unref(); // give up after 10 s
}

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

Fail the readiness probe first so the load balancer stops sending traffic. On Node 18 and earlier, idle keep-alive connections can hold server.close() open; call server.closeIdleConnections() (18.2 and later) there.

What is AsyncLocalStorage and what problem does it solve?

Seniorasyncproduction

AsyncLocalStorage keeps a value that follows one async call chain, through await and timers, without passing it as an argument. The standard use is a request id in logs.

JavaScript
const { AsyncLocalStorage } = require('node:async_hooks');
const context = new AsyncLocalStorage();

function log(message) {
  const store = context.getStore();
  console.log(`[${store ? store.requestId : 'no request'}] ${message}`);
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function handleRequest(delay) {
  await sleep(delay);
  log('loaded user');
  await sleep(delay);
  log('sent response');
}

context.run({ requestId: 'req-1' }, () => handleRequest(20));
context.run({ requestId: 'req-2' }, () => handleRequest(5));
log('server started');

The requests interleave, yet each line carries the right id. OpenTelemetry uses it for context propagation.

How do you find and fix a memory leak in Node.js?

Seniormemoryproduction

Confirm it: heap usage that keeps rising across garbage collections under steady load. Then take two heap snapshots minutes apart (--inspect or v8.writeHeapSnapshot()), compare them, and follow the retainers. Usual culprits: an unbounded Map cache, listeners added per request to a long-lived emitter (MaxListenersExceededWarning), setInterval timers never cleared, and closures that capture a whole request.

What are the most common security issues in Node.js applications?

Seniorsecurity

The common ones: injection (use parameterized SQL, and execFile with an argument array for shell commands), prototype pollution from deep-merging user JSON (below), ReDoS from a regex such as /(a+)+$/ that blocks the event loop on a crafted string, path traversal through names like ../../etc/passwd, vulnerable dependencies (npm audit, a committed lock file) and secrets in code or logs. Add helmet, rate limiting, schema validation and error responses without stack traces.

How do you prevent path traversal when serving files in Node.js?

Experiencedsecurity

Resolve the name against the base directory, then check the result is still inside it.

JavaScript
const path = require('node:path');

const UPLOADS = '/srv/uploads';

function safePath(name) {
  const full = path.resolve(UPLOADS, name);
  if (!full.startsWith(UPLOADS + path.sep)) {
    throw new Error(`blocked: ${name}`);
  }
  return full;
}

for (const name of ['report.pdf', '../../etc/passwd', 'a/../b.txt', '/etc/hosts']) {
  try {
    console.log(safePath(name));
  } catch (err) {
    console.log(err.message);
  }
}

path.resolve collapses .. and lets an absolute name replace the base, so check the resolved path, against UPLOADS + path.sep: a plain startsWith(UPLOADS) would accept /srv/uploads-old/secret.

What is prototype pollution, and how does it happen in Node.js?

Seniorsecurity

Prototype pollution is user input writing properties onto Object.prototype, so every object inherits them, usually through a hand-written deep merge.

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = merge(target[key] ?? {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, payload);

const user = {};
console.log(user.isAdmin); // Node.js prints: true

JSON.parse makes __proto__ an ordinary key, but target['__proto__'] reads the real prototype. Fix: skip __proto__, constructor and prototype, validate with a schema, or use a Map for user-keyed data.

How do you test a Node.js application?

Experiencedtesting

Unit tests for pure logic, integration tests for routes against a real test database, a few end-to-end checks. Node has a built-in runner, node:test (stable since Node 20); Jest and Vitest are common.

// sum.test.js, run with: node --test
const test = require('node:test');
const assert = require('node:assert/strict');
const { sum } = require('./sum');

test('adds two numbers', () => {
  assert.equal(sum(2, 3), 5);
});

test('rejects bad input', async () => {
  await assert.rejects(() => Promise.reject(new TypeError('bad')), TypeError);
});

For routes, export the app without listen and drive it with supertest.

What changed in recent Node.js versions that you should know for an interview?

Seniorversions
FeatureSince
Global fetchNode 18 (stable in 21)
Test runner node:testNode 18 (stable in 20)
--env-file=.envNode 20.6
import.meta.dirnameNode 20.11
require() of ES modulesNode 22.12
Running .ts by stripping typesNode 23.6 (22.18 on the 22 line)

Type stripping checks nothing and rejects syntax that needs rewriting, such as enum (Node 22 and 24 accept it with --experimental-transform-types, a flag Node 26 removed). Through Node 26, even versions become LTS; from Node 27 there is one major a year and every one becomes LTS.

How do you scale a Node.js application horizontally?

Seniorscalingproduction

Run several identical, stateless instances behind a load balancer, with shared state outside the process: sessions and rate limits in Redis, uploads in object storage, cron on a queue or one elected leader, WebSocket broadcasts through pub/sub. Use every core first, then add machines. The test: you can kill any instance and users see at most one retried request.

A payment provider sends the same webhook twice. How do you make the handler safe?

Seniorproduction

Make it idempotent: webhooks are delivered at least once, so record each event id and skip ids already seen.

JavaScript
const processed = new Set();

function handlePaymentWebhook(event) {
  if (processed.has(event.id)) return `ignored duplicate ${event.id}`;
  processed.add(event.id);
  return `marked order ${event.orderId} as paid`;
}

const deliveries = [
  { id: 'evt_1', orderId: 101 },
  { id: 'evt_2', orderId: 102 },
  { id: 'evt_1', orderId: 101 }, // the provider retried the first one
];
for (const event of deliveries) console.log(handlePaymentWebhook(event));

In production the Set is a unique constraint in the database, written in the same transaction as the side effect. Answer a duplicate with 200, or the provider keeps retrying.

Node.js coding interview questions

Small utilities and log processing problems from Node.js rounds; every solution runs here.

Implement a simple EventEmitter with on, off, once and emit.

Experiencedcodingevents

Keep a Map from event name to an array of listeners.

JavaScript
class MiniEmitter {
  #listeners = new Map();

  on(event, fn) {
    if (!this.#listeners.has(event)) this.#listeners.set(event, []);
    this.#listeners.get(event).push(fn);
    return this;
  }

  off(event, fn) {
    const list = this.#listeners.get(event) ?? [];
    this.#listeners.set(event, list.filter((l) => l !== fn && l.original !== fn));
    return this;
  }

  once(event, fn) {
    const wrapper = (...args) => {
      this.off(event, wrapper);
      fn.apply(this, args);
    };
    wrapper.original = fn;
    return this.on(event, wrapper);
  }

  emit(event, ...args) {
    const list = this.#listeners.get(event);
    if (!list || list.length === 0) {
      if (event === 'error') throw args[0];
      return false;
    }
    for (const fn of [...list]) fn.apply(this, args);
    return true;
  }
}

const e = new MiniEmitter();
const greet = (name) => console.log('hello', name);
e.on('join', greet);
e.once('join', (name) => console.log('first join was', name));
e.emit('join', 'Ada');
e.emit('join', 'Linus');
e.off('join', greet);
console.log(e.emit('join', 'nobody'));

Details that get checked: emit loops over a copy, so a once listener removing itself does not skip the next one, and off finds once wrappers through original.

Write your own promisify.

Freshercodingasync

Return a function that creates a promise and calls the original with an extra error-first callback that settles it.

JavaScript
function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn.call(this, ...args, (err, value) => {
        if (err) reject(err);
        else resolve(value);
      });
    });
  };
}

function readConfig(name, callback) {
  setTimeout(() => {
    if (name !== 'app') return callback(new Error(`no config named ${name}`));
    callback(null, { port: 8080 });
  }, 1);
}

const readConfigAsync = promisify(readConfig);
readConfigAsync('app').then((cfg) => console.log('port', cfg.port));
readConfigAsync('db').catch((err) => console.log(err.message));

A regular function, not an arrow, keeps this for methods.

Run async tasks with at most N in flight at once (a promise pool).

Seniorcodingasync

Start limit runners; each takes the next index from a shared counter until none are left.

JavaScript
async function mapLimit(items, limit, worker) {
  const results = new Array(items.length);
  let next = 0;

  async function runner() {
    while (next < items.length) {
      const i = next++;
      results[i] = await worker(items[i], i);
    }
  }

  const runners = Array.from({ length: Math.min(limit, items.length) }, runner);
  await Promise.all(runners);
  return results;
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let active = 0;
let peak = 0;

async function fetchPage(id) {
  active++;
  peak = Math.max(peak, active);
  await sleep(10 + (id % 3) * 5);
  active--;
  return `page ${id}`;
}

mapLimit([1, 2, 3, 4, 5, 6, 7], 3, fetchPage).then((pages) => {
  console.log(pages.join(', '));
  console.log('max at once:', peak);
});

next++ needs no lock, because JavaScript runs one callback at a time, and storing results by index keeps the input order.

Write a function that retries a failing async call with exponential backoff.

Experiencedcodingasync

Loop over attempts, await the call inside try, and on failure wait base * 2^attempt.

JavaScript
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function retry(fn, { retries = 3, baseMs = 10 } = {}) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fn(attempt);
    } catch (err) {
      if (attempt >= retries) throw err;
      const delay = baseMs * 2 ** attempt;
      console.log(`attempt ${attempt + 1} failed (${err.message}), retrying in ${delay} ms`);
      await sleep(delay);
    }
  }
}

let calls = 0;
async function flakyRequest() {
  calls++;
  if (calls < 3) throw new Error('ECONNRESET');
  return `ok after ${calls} calls`;
}

retry(flakyRequest).then(console.log);

Add random jitter, retry only transient errors (ECONNRESET, 503, 429), and never retry a payment without an idempotency key.

Implement an LRU cache in JavaScript.

Experiencedcodingcaching

A Map keeps insertion order, so if every read moves its key to the end, the first key is the least recently used.

JavaScript
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.map = new Map();
  }

  get(key) {
    if (!this.map.has(key)) return undefined;
    const value = this.map.get(key);
    this.map.delete(key); // move to the newest position
    this.map.set(key, value);
    return value;
  }

  set(key, value) {
    if (this.map.has(key)) this.map.delete(key);
    this.map.set(key, value);
    if (this.map.size > this.capacity) {
      const oldest = this.map.keys().next().value;
      this.map.delete(oldest);
    }
  }
}

const cache = new LRUCache(2);
cache.set('a', 1);
cache.set('b', 2);
cache.get('a');    // a is now the most recent
cache.set('c', 3); // evicts b, the least recently used
console.log([...cache.map.keys()].join(','), cache.get('b'));

Both operations are O(1). The classic version is a hash map plus a doubly linked list.

Find the most frequent words in a large text stream.

Experiencedcodingstreams

Count with a Map while streaming, and carry over a word cut in half at a chunk boundary.

JavaScript
const { Readable } = require('node:stream');

async function topWords(stream, k) {
  const counts = new Map();
  const add = (w) => counts.set(w, (counts.get(w) ?? 0) + 1);
  let leftover = '';
  for await (const chunk of stream) {
    const words = (leftover + chunk.toString()).toLowerCase().split(/\s+/);
    leftover = words.pop(); // the last word may continue in the next chunk
    for (const w of words) if (w) add(w);
  }
  if (leftover) add(leftover);
  return [...counts]
    .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
    .slice(0, k);
}

// the chunks split "dog" on purpose, like a real stream can
const source = Readable.from(['the cat and the d', 'og and the bird ', 'saw the cat']);
topWords(source, 3).then((top) => {
  for (const [word, n] of top) console.log(word, n);
});

Without leftover, d and og count as two words. For a large vocabulary and small k, use a min-heap of size k: O(n log k).

Given packages and their dependencies, print an install order and detect cycles.

Seniorcodinggraphs

A topological sort. Kahn's algorithm: start with packages that have no unmet dependencies and release each package once everything it needs is installed.

JavaScript
function installOrder(deps) {
  const pending = new Map();    // package -> number of unmet dependencies
  const dependents = new Map(); // package -> packages that need it
  for (const [pkg, needs] of Object.entries(deps)) {
    pending.set(pkg, (pending.get(pkg) ?? 0) + needs.length);
    for (const d of needs) {
      if (!pending.has(d)) pending.set(d, 0);
      if (!dependents.has(d)) dependents.set(d, []);
      dependents.get(d).push(pkg);
    }
  }
  const ready = [...pending].filter(([, n]) => n === 0).map(([pkg]) => pkg);
  const order = [];
  while (ready.length) {
    const pkg = ready.shift();
    order.push(pkg);
    for (const next of dependents.get(pkg) ?? []) {
      pending.set(next, pending.get(next) - 1);
      if (pending.get(next) === 0) ready.push(next);
    }
  }
  if (order.length !== pending.size) throw new Error('dependency cycle');
  return order;
}

console.log(installOrder({
  app: ['express', 'pg'],
  express: ['body-parser'],
  'body-parser': [],
  pg: [],
}).join(' > '));

try {
  installOrder({ a: ['b'], b: ['a'] });
} catch (err) {
  console.log(err.message);
}

O(V + E); leftover packages mean a cycle. See the topological sort visualization.

From request logs with start and end times, find the peak number of concurrent requests.

Experiencedcoding

Sweep the timeline: +1 at each start, -1 at each end, sorted, with a running total.

JavaScript
function peakConcurrency(requests) {
  const events = [];
  for (const { start, end } of requests) {
    events.push([start, 1]);
    events.push([end, -1]);
  }
  // at equal times, ends come before starts: a request ending at 30
  // does not overlap one starting at 30
  events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  let current = 0;
  let peak = 0;
  for (const [, change] of events) {
    current += change;
    peak = Math.max(peak, current);
  }
  return peak;
}

const log = [
  { start: 0, end: 30 },
  { start: 5, end: 10 },
  { start: 15, end: 20 },
  { start: 8, end: 18 },
  { start: 30, end: 40 },
];
console.log(peakConcurrency(log));

O(n log n). It is the meeting rooms problem, and it sizes a connection pool from real traffic.

Merge two log files that are each sorted by timestamp into one sorted log.

Experiencedcoding

Two pointers: always take the earlier of the two current lines.

JavaScript
function mergeLogs(a, b) {
  const out = [];
  let i = 0;
  let j = 0;
  while (i < a.length && j < b.length) {
    out.push(a[i].ts <= b[j].ts ? a[i++] : b[j++]);
  }
  while (i < a.length) out.push(a[i++]);
  while (j < b.length) out.push(b[j++]);
  return out;
}

const api = [
  { ts: 1, msg: 'api start' },
  { ts: 4, msg: 'api request' },
  { ts: 9, msg: 'api stop' },
];
const db = [
  { ts: 2, msg: 'db connect' },
  { ts: 4, msg: 'db query' },
  { ts: 7, msg: 'db slow query' },
];
for (const line of mergeLogs(api, db)) console.log(line.ts, line.msg);

<= keeps the merge stable at ties; O(n + m). For k files, keep each file's current line in a min-heap.

Preparing for the interview

How do I prepare for a Node.js interview?
Start with the event loop, because most answers lean on it: the phases, process.nextTick versus promises versus timers, and why blocking code hurts. Then cover modules, streams, error handling and Express. Build one small API with validation, a database and error middleware, and finish with a few coding problems in JavaScript, solved out loud.
What Node.js questions are asked to freshers and to experienced developers?
Freshers get definitions with a short example: what Node.js is, blocking versus non-blocking, npm, module.exports, promises and basic Express. With 3 to 5 years, expect the event loop in detail, streams and backpressure, and error handling. Senior rounds ask about production: scaling, memory leaks, graceful shutdown, security and design trade-offs.
Do I need to know Express for a Node.js interview?
Usually yes. Most Node.js jobs use Express or a framework with the same ideas (Fastify, NestJS, Koa), so expect questions on middleware, routing and error handling. Knowing the built-in http module lets you explain what the framework does for you.
Are DSA and coding problems part of Node.js interviews?
Often, especially at product companies and in campus placements: one or two array, string or hash map problems solved in JavaScript. Node.js rounds add practical tasks such as a promise pool, a retry helper or processing a log file with streams. The problems linked on this page cover the common patterns.
How long does it take to prepare for a Node.js interview?
If you already write JavaScript, two to three weeks: one on the core questions and the event loop, one on streams, errors and Express with a small project, and the rest on coding problems and mock interviews. Coming from another language, learn closures, this, promises and async/await first.
Coddy programming languages illustration

Learn to code with Coddy

GET STARTED