Node.js Interview Questions
Node.js Interview Questions
Section titled “Node.js Interview Questions”Core Architecture
Section titled “Core Architecture”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):
- timers — setTimeout, setInterval callbacks
- pending callbacks — I/O errors from previous cycle
- idle, prepare — internal use
- poll — retrieve new I/O events; execute I/O callbacks
- check — setImmediate callbacks
- 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.
Async Patterns
Section titled “Async Patterns”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 awaitasync 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); }});Modules
Section titled “Modules”Q: What is the difference between CommonJS and ES Modules?
| Feature | CommonJS | ES Modules |
|---|---|---|
| Import syntax | require() | import |
| Export syntax | module.exports | export |
| Loading | Synchronous | Asynchronous |
| File extension | .js | .mjs or "type":"module" |
__dirname | Available | Use 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.
Streams and Performance
Section titled “Streams and Performance”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.
Practical
Section titled “Practical”Q: How do you avoid blocking the event loop?
- Use async I/O — never use
readFileSync,execSyncetc. 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).