10 Node.js Interview Questions with Answers and Runnable Examples

Quick Overview
Practice Node.js interview questions on event-loop ordering, streams, modules, Express errors, workers, memory, and security with examples and follow-ups.
A useful Node.js interview answer explains where work runs, what can fail, and how you would verify the behavior. Naming an event-loop phase is less useful than explaining why one CPU-heavy request delays unrelated clients.
These ten questions pair concise sample answers with runnable traces and follow-ups. Examples use explicit .cjs or .mjs files so module context is clear. The Express example targets Express 5; check your application's runtime and framework versions before transferring an answer to production.
1. How does the Node.js event loop keep a server responsive?
Sample answer: JavaScript callbacks normally execute on one thread per Node.js isolate. Node can wait for network readiness through the operating system and use a worker pool for some other operations. The loop dispatches ready callbacks, but a long JavaScript callback prevents other callbacks on that loop from running.
Recognize timers, pending callbacks, poll, check, and close callbacks; idle/prepare are internal phases. Timers specify a minimum delay, not a deadline. Since libuv 1.45, used in Node.js 20 and later, timer processing occurs after poll. Node's event-loop guide explains the version boundary and phase behavior.
Follow-up: Does async make expensive JavaScript parallel? No. An async function still runs synchronous work on its calling thread. For a slow service, compare CPU usage, event-loop delay, dependency latency, and queued work before choosing a fix.
2. What runs first: nextTick, a Promise, or a timer?
Sample answer: First identify the execution context. Do not give one ordering rule for every CommonJS, ES-module, and nested-callback example.
Save this exact trace as order.cjs:
console.log('start');
process.nextTick(() => console.log('tick'));
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
console.log('end');
Running node order.cjs prints start, end, tick, promise, microtask. Save the same text as order.mjs: its top-level evaluation prints start, end, promise, microtask, tick. ES-module evaluation is already occurring through the microtask machinery. The process scheduling reference documents this distinction.
Recursive next-tick callbacks can prevent progress to I/O. Also avoid promising an order between top-level setTimeout(..., 0) and setImmediate(). When both are scheduled inside an I/O callback, the immediate runs before that timer.
Follow-up: Predict the output, state the file type and Node version, then run it. A trace should test a specific claim rather than depend on incidental machine timing.
3. Which asynchronous operations use libuv's thread pool?
Sample answer: Asynchronous does not mean that each request gets its own JavaScript thread. Async filesystem operations, dns.lookup(), selected crypto APIs, and asynchronous zlib work use the shared libuv pool. Ordinary network readiness generally uses operating-system facilities. Synchronous APIs still block their calling thread.
The pool defaults to four threads. A burst of expensive password hashing can delay unrelated filesystem or lookup work sharing it. The UV_THREADPOOL_SIZE documentation lists the APIs and warns that assigning the variable inside application code is not a reliable way to configure an already initialized pool.
Follow-up: Would increasing the pool fix every slow endpoint? No. First identify the congested resource. More threads cannot fix CPU-heavy JavaScript on the main loop, and can increase contention. Bound admitted work and benchmark the actual workload before changing capacity.
Practice repairing an Express authentication endpoint to connect validation, asynchronous password checking, and resource limits.
4. How do streams and backpressure prevent excessive buffering?
Sample answer: A stream lets a producer and consumer exchange chunks. Backpressure tells the producer to slow down. A writable's write() returning false means stop issuing more writes until it can resume, normally on drain; it does not mean the chunk was rejected. highWaterMark is a buffering threshold, not a guaranteed process-memory cap.
For a local file-copy exercise, save this as copy.mjs. Create input.txt first and use a fresh output path:
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
try {
await pipeline(
createReadStream('input.txt'),
createWriteStream('output.txt', { flags: 'wx' })
);
console.log('Copied');
} catch (error) {
console.error(`Copy failed: ${error.code ?? error.name}`);
process.exitCode = 1;
}
The stream API defines pipeline error propagation and cleanup. wx prevents overwriting an existing destination. A failed copy can leave partial output: choose a temporary-file and commit policy when completeness matters. For HTTP responses, pipeline failure may destroy the socket, so a JSON error response is not always still possible.
Follow-up: Test a slow consumer and a destination error. Continue with stream failures and partial writes.
5. How do CommonJS and ES modules differ?
Sample answer: CommonJS uses require() and module.exports; ESM uses import and export. Make package intent explicit with .cjs, .mjs, or the nearest package.json type field. Calling dynamic import() alone does not convert its calling file into an ES module. See Node's package rules.
ESM imports are live bindings. A CommonJS require() returns the exported value, which can be a shared mutable object. Saying CommonJS always copies everything is incorrect; destructuring an object's property into a local variable is a separate operation.
Current Node releases can also require eligible synchronous ES modules. This requires a supported runtime and a dependency graph without top-level await; older blanket statements that require() can never load ESM are misleading.
Follow-up: For a migration, inspect package exports, the exact Node version, and dependencies before changing type. Test both entry points you actually support, including an asynchronous dependency.
6. How should Express route errors reach a central handler?
Sample answer: Middleware runs in registration order. Body parsing must precede routes that need the parsed body; centralized error middleware comes after routes. Express 5 forwards rejections from returned route-handler promises. Express 4 requires explicit forwarding, commonly through a wrapper. A detached task is outside that returned chain.
This deliberately failing Express 5 app isolates the error path; it is not an authentication implementation:
// demo.cjs; requires Express 5 in this project's dependencies.
const express = require('express');
const app = express();
app.get('/demo', async (_req, _res) => {
await Promise.resolve();
throw new Error('Demonstration failure');
});
app.use((error, _req, res, next) => {
if (res.headersSent) return next(error);
console.error(error.message);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000, '127.0.0.1');
Requesting /demo returns a generic 500 response, without exposing the exception to the client. Once headers are sent, delegate instead of trying to send a second response. The Express error-handling guide documents both cases.
Follow-up: Test rejection before any response, failure after headers, and a callback error. Explore middleware and response lifecycles for longer traces.
7. When would you choose workers, processes, or more replicas?
Sample answer: Match the execution boundary to the problem:
| Choice | Useful for | Design concern |
|---|---|---|
| Worker-thread pool | CPU-heavy JavaScript | Queue limits and message cost |
| Separate processes or replicas | Independent server instances | Coordination and external state |
| Child process | An external executable | Input handling and process lifetime |
Worker threads execute JavaScript in parallel with separate isolates. Ordinary objects are not automatically shared; messages can be cloned, buffers transferred, or shared memory coordinated explicitly. Reuse a bounded pool instead of starting a worker per small task.
Cluster uses separate processes and can share a listening port. Container replicas offer another deployment choice. A process-local session map or limiter will not automatically become consistent across instances.
For child processes, avoid constructing shell command strings from user input. Argument arrays without a shell remove shell interpretation, but the executable's own options and inputs still require validation.
Follow-up: Explain overload behavior: reject, queue with a limit, or shed work. Adding workers without bounding the queue can move the failure into memory.
8. How would you investigate growing memory?
Sample answer: Reproduce the workload and compare memory after equivalent work and cleanup. Separate JavaScript heap growth from RSS, external allocations, and buffers. A rising RSS graph alone does not prove a JavaScript object leak.
Inspect retained objects and their paths back to roots. Look for unbounded caches, queued work, request listeners, timers, and closures retaining large objects. Change the ownership or eviction policy, rerun the workload, and check that retained memory stabilizes.
Node's heap-snapshot guide warns that taking a snapshot stops main-thread work and can require enough additional memory to crash the process. Diagnose on a safe replica or controlled reproduction and protect snapshots, which may contain application data.
Follow-up: Increasing the heap ceiling buys capacity; it does not repair unintended retention. State what observation would distinguish a bounded cache warming up from an indefinitely growing collection.
9. Are EventEmitter listeners asynchronous?
Sample answer: Ordinary EventEmitter listeners are invoked synchronously in registration order. An async listener can start asynchronous work, but emit() does not await the promise it returns. An unhandled error event throws. The events reference explains these semantics and rejection-capture options.
Save this as events.cjs:
const { EventEmitter } = require('node:events');
const bus = new EventEmitter();
const listener = () => console.log('listener');
bus.on('saved', listener);
try {
console.log('before');
bus.emit('saved');
console.log('after');
} finally {
bus.off('saved', listener);
}
console.log(bus.listenerCount('saved')); // 0
The trace is before, listener, after, then 0. Cleanup belongs to the listener owner's lifecycle. Raising the maximum-listener warning threshold does not remove retained listeners.
Follow-up: What survives a process crash? EventEmitter is an in-process mechanism without durable delivery. A cross-service event needs an explicit delivery, retry, and persistence contract.
10. How do you make failures and security controls hold under load?
Sample answer: Validate inputs, enforce authorization for each resource, bound work, and keep client errors separate from internal diagnostics. Use parameterized database operations rather than relying on generic sanitization. Configure session, cookie, transport, and proxy behavior for the actual deployment. Express's production security guidance is a starting point.
A limiter held in one process is not automatically a global limit. Practice the configurable Node.js token-bucket question and explain what changes when several instances accept requests. For ownership and persistence debugging, use the Node.js car-rental backend exercise.
Catch expected request failures at their boundary. Do not install an uncaughtException handler to continue normal service in an unknown state. Node's process reference calls for synchronous cleanup and shutdown after an uncaught exception, with external supervision. A planned graceful shutdown is different: stop admitting requests, drain with a deadline, close resources, then exit. Unhandled-rejection behavior also depends on runtime flags and installed handlers; it is not a substitute for handling promises deliberately.
Follow-up: Test failure as well as success: an invalid body, denied ownership, a slow dependency, a full queue, and a disconnected client. For each, describe the response, resource cleanup, and evidence you would inspect.
A focused practice session
Choose one ordering trace, one streaming failure, and one endpoint-debugging question. Explain the expected behavior first, run the smallest reproduction, then revise the answer using the result. Keep version-sensitive claims attached to a runtime and context. That produces answers you can defend when the follow-up changes one detail.
Comments (0)