Skip to content

Node.js Interview Questions

Q: What is the Node.js event loop?

The event loop is what allows Node.js to perform non-blocking I/O despite being single-threaded. When an async operation (file read, network request) is started, Node.js hands it off to the OS. When the operation completes, a callback is placed in the event queue. The event loop continuously processes the queue — executing one callback at a time.

Phases (simplified):

  1. timers — setTimeout, setInterval callbacks
  2. pending callbacks — I/O errors from previous cycle
  3. idle, prepare — internal use
  4. poll — retrieve new I/O events; execute I/O callbacks
  5. check — setImmediate callbacks
  6. close callbacks — e.g., socket.on(‘close’)

Q: Why is Node.js good for I/O-heavy workloads but not CPU-heavy ones?

Node.js uses async I/O — while waiting for a database response or file read, the event loop handles other requests. This makes it highly efficient for many concurrent I/O operations.

CPU-heavy tasks (image processing, complex calculations) block the event loop because JavaScript runs on a single thread. During a blocking computation, no other requests can be served. Solutions: Worker Threads (worker_threads module) or offloading to a separate service.


Q: What is the difference between process.nextTick and setImmediate?

process.nextTick callbacks run before the event loop continues — before any I/O events are processed. setImmediate runs in the check phase, after I/O events.

nextTick fires before the current operation completes; setImmediate fires in the next iteration of the event loop.


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

Callback hell is deeply nested callbacks that make code hard to read and reason about:

readFile(path, (err, data) => {
parseJson(data, (err, obj) => {
fetchUser(obj.id, (err, user) => {
saveResult(user, (err) => { ... });
});
});
});

Solutions: Promises and async/await flatten the chain:

const data = await readFile(path, 'utf-8');
const obj = JSON.parse(data);
const user = await fetchUser(obj.id);
await saveResult(user);

Q: What is the difference between Promise.all and Promise.allSettled?

Promise.all — rejects immediately if any promise rejects. All input promises run in parallel.

Promise.allSettled — waits for all promises to settle (resolve or reject), then returns an array of results with { status, value } or { status, reason }. Use when you want results from all promises even if some fail.


Q: How do you handle errors in async functions?

// try/catch for await
async function handler(req, res) {
try {
const user = await userService.findById(req.params.id);
res.json(user);
} catch (err) {
res.status(500).json({ error: err.message });
}
}
// Or propagate the error to an Express error handler via next()
app.get('/users/:id', async (req, res, next) => {
try {
res.json(await userService.findById(req.params.id));
} catch (err) {
next(err);
}
});

Q: What is the difference between CommonJS and ES Modules?

FeatureCommonJSES Modules
Import syntaxrequire()import
Export syntaxmodule.exportsexport
LoadingSynchronousAsynchronous
File extension.js.mjs or "type":"module"
__dirnameAvailableUse fileURLToPath(import.meta.url)

Node.js supports both. New projects should use ESM.


Q: What is module caching in Node.js?

When you require() a module for the first time, Node.js loads and caches it. Subsequent require() calls return the cached version. This means module-level state is shared across all importers.


Q: What are streams and why use them?

Streams process data incrementally rather than loading everything into memory. For a 1GB file:

  • Without streams: load 1GB into memory, then process
  • With streams: read a chunk, process it, read the next chunk — memory usage stays small

Node.js uses streams internally for HTTP requests and responses, file I/O, and TCP sockets.


Q: What is backpressure?

When a writable stream can’t keep up with a readable stream, backpressure signals the readable to slow down. Without it, the readable fills memory with unprocessed data.

pipe() and pipeline() handle backpressure automatically. When writing manually, check the return value of writable.write() — if false, wait for the drain event before writing more.


Q: How do you avoid blocking the event loop?

  • Use async I/O — never use readFileSync, execSync etc. in request handlers
  • Offload CPU-intensive work to Worker Threads
  • Break up large synchronous operations with setImmediate
  • Use streaming instead of loading large data sets at once

Q: What is the difference between cluster and worker_threads?

cluster creates multiple Node.js processes (separate memory, separate event loops) that share a TCP port. Each process handles requests independently. Good for scaling HTTP servers across CPU cores.

worker_threads creates threads within a single process that share memory. Good for CPU-intensive tasks that need shared data (image processing, heavy computation).