The short answer: Node.js interviews test whether you understand how Node runs your code: the event loop and its phases, process.nextTick vs promises vs setImmediate, what runs on the libuv thread pool, and what blocks everything. Expect questions on async error handling, streams and backpressure, CommonJS vs ES modules, scaling with worker threads, child processes and clusters, plus Express, security and testing. Many teams also check that you know current Node. Node 26 became the active long-term support (LTS) line on October 28, 2026; Node runs TypeScript files by stripping their types; require() can load ES modules; and starting with Node 27, there is one major release a year. The answers below are checked against the Node.js documentation and release notes, with sources at the end.
Node is JavaScript on the server, so make sure the language itself is solid first: see our JavaScript interview questions.
Node.js versions in 2026
| Version | Status on 4 November 2026 | End of life |
|---|---|---|
| Node 26 (“Lithium”) | Active LTS since October 28, 2026 | April 30, 2029 |
| Node 24 (“Krypton”) | Maintenance LTS since October 20, 2026 | April 30, 2028 |
| Node 22 (“Jod”) | Maintenance LTS | April 30, 2027 |
| Node 20 (“Iron”) | End of life | April 30, 2026 |
| Node 27 | Alpha since October 28, 2026; becomes Current in April 2027 | April 30, 2030 |
Until Node 26, odd-numbered versions were short-lived and even-numbered ones became LTS. Starting with Node 27, “the release cycle will be annual”: every major version gets about six months of alpha and six months as Current, then becomes LTS, with long-term support that “typically guarantees that critical bugs will be fixed for a total of 30 months.” Production apps should run an LTS line.
What interviewers ask about from recent releases:
- TypeScript without a build step: type stripping is on by default since v22.18.0 and v23.6.0, and stable since v24.12.0 and v25.2.0.
require()of ES modules: no longer behind a flag since v22.12.0 and v20.19.0, and no longer experimental since v25.4.0.- Built-in tooling: the test runner (
node --test) is stable since v20, watch mode (--watch) since v22.0.0 and v20.13.0,--env-filesince v24.10.0 and v22.21.0, and the permission model since v23.5.0 and v22.13.0. - Node 24 (May 2025): V8 13.6, npm 11, a global
URLPattern, andurl.parse()deprecated at runtime in favor of the WHATWGURLAPI. - Node 26 (May 2026): V8 14.6, the Temporal date API enabled by default, Undici 8 behind the built-in
fetch, and the legacy_stream_*modules removed.
Fundamentals
“What is Node.js?”
Node.js is a JavaScript runtime built on Google’s V8 engine and the libuv library. It runs your JavaScript on one main thread and handles I/O (files, network, timers) without blocking: it starts an operation, keeps running other code, and runs your callback when the result is ready. That makes it efficient for I/O-heavy work like APIs, proxies and real-time apps, and a poor fit for long CPU-bound computation on the main thread. Unlike browser JavaScript, there’s no DOM; instead there are modules like fs, http, crypto and the process object.
“Is Node.js single-threaded?”
Your JavaScript runs on a single thread, but Node itself isn’t. libuv keeps a thread pool, four threads by default (UV_THREADPOOL_SIZE), used by “all fs APIs” except watchers and explicitly synchronous ones, by asynchronous crypto such as crypto.pbkdf2() and crypto.scrypt(), by dns.lookup() and by zlib. Network sockets don’t use the pool; they rely on the operating system’s async mechanisms. You can also create threads yourself with worker_threads. A good answer: “single-threaded JavaScript, multi-threaded runtime.”
“What blocks the event loop, and why does it matter?”
Anything that keeps the main thread busy: a long synchronous loop, JSON.parse on a huge payload, synchronous APIs like fs.readFileSync inside a request handler, a catastrophic regular expression. While it runs, no other request is served, no timer fires and no I/O callback runs. Fixes: use async APIs, break work into chunks, move CPU-heavy work to a worker thread, and stream large data instead of loading it all at once. To find the culprit, profile with node --cpu-prof or the inspector (node --inspect).
The event loop
“Walk me through the event loop phases.”
Each turn of the loop goes through these phases, in order:
- Timers: callbacks from
setTimeout()andsetInterval()whose time has come. - Pending callbacks: some I/O callbacks deferred from the previous turn.
- Idle, prepare: internal use only.
- Poll: retrieves new I/O events and runs their callbacks; Node “will block here when appropriate” when there’s nothing else to do.
- Check:
setImmediate()callbacks. - Close callbacks: for example
socket.on('close').
Between operations, Node drains the process.nextTick queue and then the promise (microtask) queue.
“process.nextTick, setImmediate or setTimeout(fn, 0)?”
process.nextTick(fn)runs after the current operation finishes, before the event loop continues, “regardless of the current phase.” RecursivenextTickcalls can “starve” I/O.- Promise callbacks and
queueMicrotask(fn)run in the microtask queue. In CommonJS files,nextTickcallbacks run before microtasks; in ES modules, the opposite. setImmediate(fn)runs in the check phase, after I/O polling.setTimeout(fn, 0)runs in a later timers phase.
The names are confusing: nextTick actually fires sooner than setImmediate, and the Node docs admit the names should be swapped. Their advice: use setImmediate() “in all cases because it’s easier to reason about,” and prefer queueMicrotask() over nextTick unless you need its specific behavior.
Async patterns and error handling
“Callbacks, promises or async/await?”
Node’s core APIs started with error-first callbacks ((err, result) => {}), which nest badly (“callback hell”). Promises, and async/await on top of them, make sequential and parallel flows readable and let one try/catch handle errors. Most core modules now have promise versions (node:fs/promises, node:timers/promises), and util.promisify wraps old callback APIs. Know the combinators: Promise.all (fails fast), Promise.allSettled (waits for all), Promise.race and Promise.any.
“What happens with an unhandled promise rejection?”
By default the process crashes: the --unhandled-rejections mode defaults to throw, so an unhandled rejection is “raised as an uncaught exception.” process.on('uncaughtException') is, in the docs’ words, “a crude mechanism for exception handling intended to be used only as a last resort,” and “it is not safe to resume normal operation” after it. The right approach: handle errors where they happen, log them, and let a process manager or orchestrator restart the process if it does crash.
“How does EventEmitter work?”
Objects like servers and streams emit named events; listeners subscribe with on or once, and emit calls them synchronously in order. The 'error' event is special: if it’s emitted with no 'error' listener, the error is thrown and, unhandled, crashes the process. Remove listeners you no longer need, or you’ll leak memory, which Node hints at with its “possible EventEmitter memory leak” warning.
Streams and buffers
“What are streams, and why use them?”
Streams process data piece by piece instead of loading it all into memory. There are four types: Readable (fs.createReadStream, an HTTP request), Writable (fs.createWriteStream, an HTTP response), Duplex (a TCP socket) and Transform (zlib.createGzip). Streaming a 5 GB file through gzip uses a small, fixed amount of memory; reading it whole might not fit at all. A Buffer is Node’s container for raw binary data, which is what most streams carry.
“What is backpressure?”
Backpressure is what happens when data arrives faster than the consumer can handle it: “a buildup of data behind a buffer.” When a writable’s buffer is full, write() returns false, and you must wait for the 'drain' event before writing more. pipe() handles this for you, and stream.pipeline() handles it too while also “forwarding errors and properly cleaning up”:
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';
await pipeline(
createReadStream('access.log'),
createGzip(),
createWriteStream('access.log.gz')
);
Modules
“CommonJS or ES modules?”
CommonJS uses require() and module.exports, loads synchronously, and was Node’s original system. ES modules use import and export, are the JavaScript standard, load asynchronously, and support top-level await. Node treats a file as ESM if it ends in .mjs, or ends in .js with "type": "module" in the nearest package.json; .cjs is always CommonJS. In ESM, __dirname and __filename don’t exist; use import.meta.dirname and import.meta.filename. Package authors use the "exports" field in package.json to define entry points.
“Can CommonJS load an ES module?”
Yes, in current versions: require() can load an ES module as long as it’s “fully synchronous (contains no top-level await).” It returns the module namespace object, with the default export on .default. If the module graph has top-level await, Node throws ERR_REQUIRE_ASYNC_MODULE; use import() instead. This ended years of friction between the two systems for library authors.
Scaling Node.js
“Worker threads, child processes or cluster?”
worker_threadsrun JavaScript in parallel threads in the same process. The docs: they’re “useful for performing CPU-intensive JavaScript operations” and “do not help much with I/O-intensive work.” Unlike the other two, workers can share memory throughSharedArrayBuffer. Use a pool, since creating a worker per task costs more than it saves.child_processruns a separate program or Node process, for example a shell command or an image tool.clusterforks several Node processes that share one server port, to use all CPU cores. In containers, many teams run one process per container and scale with the orchestrator instead.
“How would you scale a Node API?”
Make it stateless (sessions in a store like Redis, files in object storage), run several instances behind a load balancer, cache what you can, move slow or CPU-heavy work to a queue processed by background workers, and watch event loop delay, memory and response times. Stateless, multiple instances, and queues for slow work: that’s the core of the answer.
Express and APIs
“What is middleware in Express?”
A function (req, res, next) that runs during the request: it can read or change the request, end the response, or call next() to pass control on. Order matters: logging and body parsing come first, routes next, and the error handler, with four arguments (err, req, res, next), last.
“What changed in Express 5?”
Express 5 requires Node.js 18 or higher. The biggest change for everyday code: “middleware and handlers that return rejected promises are now handled by forwarding the rejected value as an Error to the error handling middleware.” In Express 4, an async handler that threw left you with an unhandled rejection unless you caught it yourself. Express 5 also changed route patterns (a wildcard must be named, /*splat instead of /*), removed app.del() and req.param(), and req.body is now undefined, not {}, when no body parser ran.
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id); // a rejection goes to the error handler
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.use((err, req, res, next) => {
req.log?.error(err);
res.status(500).json({ error: 'Something went wrong' });
});
For REST design, status codes and API security, see our web API interview questions.
Security
“How do you secure a Node.js application?”
- Validate input at the edge with a schema, and never build SQL or shell commands from strings: use parameterized queries and pass arguments to
execFileas an array. - Keep secrets out of code: environment variables or a secrets manager, never committed
.envfiles. - Manage dependencies: commit the lockfile, install with
npm ci, review new packages, and runnpm auditin CI. - Limit damage: rate limiting, security headers, small request body limits, running as a non-root user, and the permission model (
--permission), which blocks file system access, child processes, worker threads and other capabilities unless you allow them with flags like--allow-fs-read. - Fail safely: don’t leak stack traces to clients; log them server-side.
Debugging and performance
“How do you find a memory leak?”
Watch memory over time (for example process.memoryUsage() in metrics). If heap usage keeps growing under steady load, take heap snapshots a few minutes apart with the inspector and compare what’s growing. Common causes: caches without limits, event listeners added on every request and never removed, closures holding large objects, and global arrays used as queues. For CPU problems, record a profile with --cpu-prof or the inspector and look for the hottest functions.
“How do you pass request context, like a request ID, through async code?”
With AsyncLocalStorage from node:async_hooks: run the request inside als.run(store, ...), and any code called from it, including after await, can read the store with als.getStore(). It saves passing a logger or request ID through every function. Since Node 24 it uses a more efficient implementation, AsyncContextFrame, by default.
Testing and TypeScript
“How do you test Node.js code?”
Node has a stable built-in test runner since v20: node --test with describe/it or test, assertions from node:assert, function mocks (mock.fn), fake timers (mock.timers) and snapshot tests (stable since v23.4.0). Code coverage (--experimental-test-coverage) is still experimental. Many teams use Vitest or Jest instead; know one well. Test HTTP handlers through the app (for example with Supertest), and test against a real database in a container rather than mocking every query.
“Can Node run TypeScript directly?”
Yes, with limits. Node “will replace TypeScript syntax with whitespace, and no type checking is performed,” so run tsc --noEmit in CI. Only erasable syntax works: enums, namespaces with runtime code, parameter properties and decorators fail, and Node 26 removed the --experimental-transform-types flag that used to support some of them. Imports need the .ts extension, type-only imports need import type, and tsconfig.json is ignored, so paths aliases don’t work. More in our TypeScript interview questions.
Output puzzles (“what does this print?”)
Timers, ticks and promises (in a CommonJS file)
console.log('1');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('2');
Answer: 1, 2, nextTick, promise, then timeout and immediate in either order. Synchronous code runs first, then the nextTick queue, then microtasks. In the main module, the order of setTimeout(0) and setImmediate “is non-deterministic.” In an ES module, promise would print before nextTick.
The same two timers, inside an I/O callback
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
Answer: always immediate, then timeout. After the poll phase comes the check phase (setImmediate), and timers wait for the next turn of the loop.
An error event nobody listens to
const emitter = new EventEmitter();
emitter.emit('error', new Error('boom'));
console.log('after');
Answer: the error is thrown, the process crashes with Error: boom, and after never prints. Always attach an 'error' listener to emitters and streams.
require() of a module with top-level await
config.mjs contains export const settings = await loadSettings(); and a CommonJS file runs require('./config.mjs'). Answer: Node throws ERR_REQUIRE_ASYNC_MODULE. Load it with await import('./config.mjs') instead.
Coding exercises with solutions
Run promises with a concurrency limit
A favorite: process 1,000 URLs, at most five at a time.
async function mapLimit(items, limit, fn) {
const results = new Array(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const i = next++;
results[i] = await fn(items[i], i);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return results;
}
Why it works: JavaScript runs on one thread, so next++ can’t be claimed twice. Follow-ups: stop on the first error, or collect errors like allSettled.
Retry with exponential backoff
import { setTimeout as sleep } from 'node:timers/promises';
async function retry(fn, { retries = 3, base = 200 } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt >= retries) throw err;
await sleep(base * 2 ** attempt + Math.random() * base); // backoff plus jitter
}
}
}
Discuss which errors deserve a retry (timeouts, 503s) and which don’t (a 400 or a validation error).
Count lines in a huge file without loading it
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const lines = createInterface({ input: createReadStream('app.log'), crlfDelay: Infinity });
let errors = 0;
for await (const line of lines) {
if (line.includes('ERROR')) errors++;
}
console.log(errors);
Memory stays flat whatever the file size.
Shut down gracefully
const server = app.listen(3000);
process.on('SIGTERM', () => {
server.close(async () => {
await db.close();
process.exit(0);
});
setTimeout(() => process.exit(1), 10_000).unref(); // give up after 10 s
});
server.close() stops accepting new connections and waits for open ones; containers send SIGTERM before killing the process.
More to practice: a fixed-window rate-limiting middleware, an LRU cache with a size limit, and a promisify implementation.
Questions by level
- Junior: what Node is, modules and npm,
async/awaitand error handling, the basics of the event loop, building a small Express API, environment variables. - Mid-level: event loop phases and
nextTickvssetImmediate, the thread pool, streams and backpressure, CommonJS vs ESM, middleware and Express 5 error handling, testing, debugging a slow endpoint. - Senior: scaling and statelessness, worker threads vs processes, memory leaks and profiling, graceful shutdown and health checks, observability (logs, metrics, traces), dependency and supply-chain security, and upgrade strategy now that Node ships one major version a year.
For architecture rounds, see our system design interview guide.
Behavioral questions for Node.js developers
Use the STAR method with a real example:
- “Tell me about a production incident in a Node service. How did you find the cause?”
- “Describe a performance problem you fixed: what did you measure before and after?”
- “How did you plan and roll out a Node.js major version upgrade?”
- “When did you decide Node wasn’t the right tool for a job?”
More: our backend developer interview questions and full-stack developer interview questions.
Frequently asked questions
What Node.js questions are asked in interviews?
Which Node.js version should I use in 2026?
Is Node.js single-threaded?
What is the difference between process.nextTick and setImmediate?
Can Node.js run TypeScript?
Should I learn Express or NestJS for a Node.js interview?
Can Huru help me practice Node.js interview questions?
Sources
- Node.js, Previous releases (release lines and the annual schedule from Node 27) and the Release Working Group schedule (LTS, maintenance and end-of-life dates). Read on 4 November 2026.
- Node.js release notes: Node.js 24.0.0 (May 6, 2025) and Node.js 26.0.0 (May 5, 2026); GitHub releases (26.13.0 “Lithium” LTS, October 28, 2026). Read on 4 November 2026.
- Node.js Learn: The Node.js event loop and Backpressuring in streams. Read on 4 November 2026.
- Node.js API documentation: Command-line options (thread pool,
--watch,--env-file, permission model), Modules: CommonJS (require(esm)), Process, Worker threads, Test runner and TypeScript. Read on 4 November 2026. - Express, Migrating to Express 5. Read on 4 November 2026.