Node.js From Zero
Node.js Interview Questions With Model Answers
Event loop, streams, errors, scaling and security questions with model answers.
On this page
- Words used in this lesson
- How to use this lesson
- Part 1: Runtime and the event loop
- Part 2: Promises, async and errors
- Part 3: Streams, buffers and files
- Part 4: Servers and frameworks
- Part 5: Scaling and performance
- Part 6: Security and production
- Rapid-fire round
- Common wrong answers to avoid
- Try it
- Key takeaways
- What's next
In one line: a Node.js interview is rarely about memorising API names. It tests whether you understand one thread plus an event loop, can predict what runs first, know where errors go, can scale and secure a server, and can say how you would check an answer. This lesson has 31 questions with the traps and model answers, and many answers are backed by a tiny experiment you can run in a minute.
You have now been through the whole series: the runtime and modules, the event loop, files and buffers, streams, a raw HTTP server, Express and Fastify, errors and shutdown, concurrency, and testing and production habits. Interviewers love to mix these. This lesson puts them in question form, so you can test yourself. If something feels shaky, the lesson link is next to the question.
A good answer in an interview has a shape: say the short answer first, give the reason, give a tiny example or a number, then mention the trap or the trade-off. Short, calm and specific beats long and vague. If you are not sure, say what you would run to find out. That is what a working developer does.
For wider preparation, see Backend Lesson 10 (Node.js performance basics), Security Lesson 1 (injection) and Security Lesson 6 (dependencies).
What I ran / checked. Every experiment quoted below ran on Linux with Node.js v22.22.0 on a 4-core machine (Express 5.3.0, Fastify 5.12.5, pino 10.4.0 where used). Timings vary by machine. Sources I read (in my own words): the Node.js docs for process, http, cli, worker_threads, cluster, child_process and the test runner; the Fastify docs on validation, hooks and logging; the README of the router package inside Express 5; the expressjs/express change log; and the goldbergyoni/nodebestpractices README. Could not reach: MDN, the Node.js website, the Express guide pages, and the OWASP site. Not verified: I did not run anything in Docker or Kubernetes, and I did not benchmark Express against Fastify, so I make no claim about which is faster. Answers about libraries I did not install (for example p-limit, piscina, helmet) are described from general knowledge and marked as such.
Words used in this lesson
- The event loop is the loop that picks the next ready callback and runs it on the single JavaScript thread.
- The call stack is the list of functions that are running right now.
- A microtask is a small task (promise continuation,
queueMicrotask) that runs right after the current code, before the loop moves on. - A macrotask is a bigger task from a queue: timers, I/O callbacks,
setImmediate. - libuv is the C library under Node that does the event loop, the thread pool and async I/O.
- The thread pool is a small set of threads (4 by default) that libuv uses for file system, DNS lookups and some crypto.
- Backpressure is the way a slow reader tells a fast writer to slow down.
- ESM / CJS are the two module systems:
import/exportandrequire/module.exports. - Prototype pollution is an attack that adds properties to
Object.prototype, so every object in the program gets them. - ReDoS is a denial of service from a regular expression that takes exponential time on crafted input.
- A timing attack learns a secret by measuring how long a comparison takes.
- Idempotent means doing it twice has the same effect as once.
- Graceful shutdown means finishing in-flight work before the process exits.
How to use this lesson
Read each question, answer it out loud first, then read the model answer. The tags tell you how much each one matters:
- (Must know): expected at almost every level.
- (Senior signal): shows depth and experience.
- (Trap): sounds easy, but a common wrong answer is waiting.
Part 1: Runtime and the event loop
Q1. What is Node.js, and is it a language, a framework or a runtime? (Must know)
It is a runtime: a program that runs JavaScript outside the browser. It combines the V8 engine (which runs JavaScript), libuv (event loop, thread pool, async I/O), and a library of built-in modules like fs, http and crypto. It is not a language, and it is not a web framework. Express and Fastify are frameworks that run on it. See lesson 0.
Q2. "Node.js is single-threaded." True or false? (Trap)
Half true, which is the trap. My JavaScript runs on one main thread, so two pieces of my code never run at the same instant. But Node as a whole uses more threads: libuv's thread pool (4 by default) runs file system calls, DNS lookups and some crypto, and V8 uses helper threads for garbage collection. I can also start worker threads on purpose. I can show the thread pool with a test: hashing 4 passwords in parallel with pbkdf2 took about 560 ms on 4 cores, while one hash took 333 ms. If everything were on one thread, 4 hashes would take about 1,300 ms. With UV_THREADPOOL_SIZE=1, the 4 hashes took 1,698 ms, which proves the pool is real.
Q3. Explain the event loop in plain words. (Must know)
Node runs my main script, then keeps a loop alive as long as there is something to wait for. Each turn of the loop runs the ready callbacks by phase: timers whose time has come, I/O callbacks, setImmediate callbacks and so on. Between callbacks, the microtask queues run (process.nextTick first, then promise callbacks). Long-running JavaScript blocks the loop, because nothing else can run until my function returns. See lesson 1.
Q4. What is the order: console.log, setTimeout(0), setImmediate, a promise then, process.nextTick, queueMicrotask? (Trap)
The trap is that the answer depends on whether the file is CommonJS or an ES module. I ran the same six lines both ways.
CommonJS (.cjs):
1 sync2 nextTick3 promise then4 queueMicrotask5 setTimeout 06 setImmediateES module (.mjs):
1 sync3 promise then4 queueMicrotask2 nextTick5 setTimeout 06 setImmediateIn an ES module, the whole file is run inside a promise job, so we are already inside the microtask queue when the code ends. The promise callbacks queued there run before the nextTick queue gets its turn. In CommonJS, nextTick goes first. Either way the order is: sync code, then microtasks, then timers and setImmediate. As for setTimeout(0) against setImmediate in the main script, I say the order is not guaranteed (it depends on process speed); inside an I/O callback, setImmediate always runs first. I tested that: in a readFile callback, io: setImmediate printed before io: setTimeout 0.
Q5. What is the difference between process.nextTick and setImmediate? (Must know)
nextTick runs its callback right after the current operation, before the event loop continues, even before promise callbacks in CommonJS. setImmediate runs in the check phase of the loop, after the I/O phase. The surprising part is the names: "tick" is actually sooner than "immediate". The risk of nextTick is starvation. I ran a function that re-queued itself with nextTick two million times, and the setTimeout(0) I had set before it only ran after all two million:
timer finally ran after 2000000 nextTicksSo recursive nextTick can block I/O and timers. For "do this soon but let I/O breathe" I use setImmediate.
Q6. What blocks the event loop, and how do you notice? (Must know)
Anything synchronous that takes long: big JSON.parse or JSON.stringify, a heavy loop, fs.readFileSync or crypto.pbkdf2Sync in a request handler, a regular expression with catastrophic backtracking, large sorts, synchronous image or PDF work. A test I ran: parsing a 24 MB JSON string blocked the loop for 276 ms, and during that time no request could be served. I notice it with perf_hooks.monitorEventLoopDelay (when I blocked the loop for 300 ms, its max read 309 ms), with a health check that gets slow, and with a CPU profile (--cpu-prof). See lesson 8.
Q7. Why is async code not automatically non-blocking? (Trap)
Because async does not move work to another thread. An async function runs synchronously until its first await. If I write a for loop with a billion iterations inside an async function, it blocks the loop just the same. async/await helps with waiting on I/O, not with calculating. For CPU work I need a worker thread, a child process, or a separate service. See lesson 7.
Q8. How do CommonJS and ES modules differ in practice? (Must know)
CommonJS uses require and module.exports. It loads synchronously, and exports are copies of values at the time of the call. ES modules use import/export, are statically analysable, and exports are live bindings. I tested that last point:
// lib.mjsexport let count = 0; export function inc(){ count++; }// main.mjsimport { count, inc } from "./lib.mjs";console.log(count); inc(); console.log(count); // 0, then 1In ESM the second log shows 1, because the import is a live view of the variable. Other differences: in ESM __dirname and require are undefined (I checked; use import.meta.dirname, which printed my folder), and top-level await works only in ESM. Also, both systems cache modules: a second require of the same file returns the same object, and I confirmed that a counter incremented through the first reference was visible through the second.
Q9. Can CommonJS load an ES module? (Senior signal)
In older Node versions it could not use require on ESM and you had to use dynamic import(). In the Node 22.22 I used, require("./esm-lib.mjs") worked without any flag and returned the exports ([ 'hi' ]). I would still check the docs for the exact Node version I deploy to, since this has changed across releases. One restriction I know of: an ES module that uses top-level await cannot be loaded this way.
Part 2: Promises, async and errors
Q10. What happens if an async function throws before any await? (Trap)
It does not throw synchronously. An async function always returns a promise, so the error becomes a rejected promise. I ran this:
async function f() { throw new Error("inside async"); }let p; try { p = f(); console.log("try/catch did NOT catch"); } catch { console.log("caught sync"); }p.catch((e) => console.log("rejection:", e.message));try/catch did NOT catch; got Promise { <rejected> Error: inside async ... }rejection: inside asyncSo I either await it inside try/catch or attach .catch. If I do neither, I get an unhandled rejection.
Q11. What does Node do with an unhandled promise rejection? (Must know)
Since Node 15 the default is to treat it like an uncaught exception: the process prints the error and exits with code 1. I ran a script with Promise.reject(...) and a timer scheduled after it, and the timer never fired. With --unhandled-rejections=warn it only warns and continues. If I add a process.on("unhandledRejection") listener the crash stops, so that listener must log the error and start shutdown, not just print. See lesson 6.
Q12. Should you keep the server running after an uncaughtException? (Trap)
No. The state is unknown after an uncaught exception: half-updated objects, leaked locks, broken connections. I log the error with full detail, begin graceful shutdown with a short timeout, and exit non-zero, so the supervisor (Docker, systemd, Kubernetes, PM2) starts a clean process. The nodebestpractices guide makes the same point: separate operational errors, which I handle, from programmer errors, after which I restart.
Q13. How do errors behave differently in Express 4 and Express 5? (Senior signal)
In Express 5, if a route handler returns a promise that rejects (which is what an async function does when it throws), the router calls next(error) for me. The router README says so, and I confirmed that both a sync throw and an async throw reached my four-argument error handler and produced a clean 500. In Express 4, a rejected promise from an async handler was not caught and the request hung, so people wrote wrapper functions. (I know the Express 4 behaviour from experience and did not install Express 4 for this series.) In both versions I still must write the error handler myself, with exactly four parameters, registered last.
Q14. How do you stop Promise.all from killing the whole batch when one call fails? (Must know)
Promise.all rejects as soon as one promise rejects. If partial success is fine, I use Promise.allSettled, which waits for all and gives {status, value} or {status, reason} for each. I tested it with a function that fails for id 2: allSettled returned ["fulfilled","rejected","fulfilled"]. Also note that Promise.all does not cancel the other operations when it rejects. If I need real cancellation, I pass an AbortSignal.
Q15. Why is array.forEach(async ...) a bug? (Trap)
forEach ignores the promises returned by the callback, so it does not wait. I ran it: the line after the loop printed an empty array, and the results arrived later. I use for...of with await for a sequence, or await Promise.all(items.map(async ...)) for parallel work, ideally with a concurrency limit.
Part 3: Streams, buffers and files
Q16. Why use streams instead of readFile for big files? (Must know)
readFile loads the whole file into memory, so memory use grows with file size, and the first byte is not sent until the last is read. A stream processes the file in chunks, so memory stays flat and the response can start immediately. Streams also provide backpressure: if the destination is slow, the source pauses instead of piling data in memory. See lesson 3.
Q17. What is the difference between pipe and pipeline? (Trap)
pipe connects streams but does not propagate errors or clean up. If the source fails, the destination stays open. I tested it: with src.pipe(dst) and a source that failed, the destination was never closed. With await pipeline(src, dst) the error was thrown to my try/catch and the destination was destroyed (destination destroyed: true). So I always use stream.pipeline (the promise version from node:stream/promises), and I attach error listeners to any stream I create myself.
Q18. What is a Buffer, and what is the danger of Buffer.allocUnsafe? (Senior signal)
A Buffer is a fixed-size block of raw bytes outside the normal JavaScript heap, used for binary data such as files and network packets. Buffer.alloc(n) gives zero-filled memory. Buffer.allocUnsafe(n) is faster because it does not clear the memory, so it can contain old data from earlier use. If I send it without filling it fully, I could leak that old data to a client. I use alloc unless I measured a need and I overwrite every byte. (This is from the Node docs and general knowledge; I did not run an experiment on it.)
Q19. Why does process.exit() sometimes lose output? (Trap)
process.exit() ends the process right away, even if there is output waiting to be written. When stdout is a pipe, writes can be asynchronous. I wrote a 5 MB string to stdout and called process.exit(0) right after, piped into wc -c, and got only 65,536 bytes, which is one pipe buffer. The safe habit is to set process.exitCode and let the process finish, or wait for the write callback before exiting.
Part 4: Servers and frameworks
Q20. How would you build a JSON API without a framework, and why would you add one? (Must know)
With node:http I create a server, read the method and URL, collect the body chunks, parse JSON, set Content-Type, and write the status and body myself. See lesson 4. It works, but I must write the router, body parsing, validation, error handling and logging by hand, and I will repeat code on every route. A framework gives me those as tested building blocks. I would stay with plain node:http for a tiny internal tool or when learning, and use Express or Fastify for anything with more than a few routes.
Q21. What is middleware and what order should it be in? (Must know)
A function (req, res, next) that runs between the request and the final handler. Express runs them in the order registered. A sensible order: request logger and request id first, security headers, body parsers, authentication, routes, a 404 handler, and the four-argument error handler last. I proved the order matters: with the logger after the JSON parser, a request with broken JSON never reached the logger. See lesson 5.
Q22. How does validation differ in Fastify and Express? (Senior signal)
Fastify has JSON Schema validation built in: I put a schema next to the route, and a request that does not match never reaches the handler. Express has none, so I use a library (Zod, Joi, Ajv) in a middleware. A subtle point with Fastify's default settings: they coerce types. In my test, the string "7" became the integer 7, true became 1, and a lone string became a one-item array. That can surprise me, so I test odd inputs and tighten the setup when I need strict rules.
Q23. How do you handle 100 requests per second where each one needs a slow database call? (Senior signal)
I/O wait is cheap in Node, so the main concerns are the limits downstream. I use a connection pool with a sensible size, timeouts on every call, and a limit on concurrent work so I do not flood the database. I make sure no request does CPU work on the main thread. I measure event loop delay and p95 latency. If the database becomes the bottleneck I look at indexes, caching or read replicas, not at Node. See lesson 7 for the limiter experiment: all-at-once gave 25 failures out of 30, a limit of 5 gave none.
Part 5: Scaling and performance
Q24. How do you scale a Node app to use all CPU cores? (Must know)
A Node process runs JavaScript on one core. Options: the cluster module, which forks worker processes that share a port (I saw three process ids answer six requests, two each); a process manager like PM2 in cluster mode; or, more commonly today, running one process per container and scaling the number of containers behind a load balancer. Worker threads are a different tool: they help one heavy task, not many requests. Whatever I choose, state must not live in process memory, because each process has its own copy. Sessions and caches go to Redis or a database.
Q25. When would you use worker threads, and when not? (Senior signal)
For CPU-heavy JavaScript in a web server: image resizing, large parsing, a pure-JS hash. In my test, fib(40) four times took 5.1 s on the main thread, with the event loop frozen for 5 s; with four workers it took 2.3 s and the loop stayed free (6 ms delay). I would not use them for I/O work, because the docs say Node's built-in async I/O is better. I would not start a worker per tiny task: 40 one-millisecond tasks took 695 ms with a new worker each time and 36 ms on the main thread. I would use a pool for tasks above tens of milliseconds, or move long jobs to a queue.
Q26. How do you find and fix a memory leak? (Must know)
First confirm it: watch heapUsed after garbage collection over time. A leak is a line that keeps rising. In my test an unbounded Map cache grew about 90 MB per 100,000 requests, and a bounded one stayed at 4.7 MB. Then find the culprit with heap snapshots: take one after warm-up, run traffic, take a second, compare in Chrome DevTools, and read the retainers of the growing objects. Common causes are unbounded caches, listeners added per request, uncleared timers, and closures that hold big objects. The fix is usually a size or time limit, or removing the listener. See lesson 8.
Part 6: Security and production
Q27. What is the risk with child_process.exec? (Must know)
exec runs its string through a shell, so user input can add commands. With the name x; echo INJECTED, exec ran both commands and printed INJECTED. execFile("echo", ["hello", name]) printed the whole text as a plain argument, because it uses no shell. I use execFile or spawn with an array of arguments, and I validate input anyway. The Node docs repeat the warning not to pass unsanitised input to the shell variants. See Security Lesson 1.
Q28. Name three Node-specific security issues and a defence for each. (Senior signal)
First, prototype pollution: a naive merge function copied __proto__ from JSON, and afterwards a brand new empty object had isAdmin: true (I ran it). Defence: reject __proto__, constructor and prototype keys, use Object.create(null) or Map for dictionaries, and use a well-maintained library. Second, ReDoS: the regex /^(a+)+$/ took 39 ms on 20 characters, 110 ms on 24 and 410 ms on 26, so each extra character multiplies the time and a long string freezes the loop. Defence: avoid nested quantifiers, limit input length, test regexes. Third, timing attacks on secret comparison: use crypto.timingSafeEqual (note it throws ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH when lengths differ, so I compare hashes of equal length). I would also mention dependency risk (npm audit, lock files) from Security Lesson 6.
Q29. Why not just keep the process alive with process.on("uncaughtException") and carry on? (Trap)
Because the process is in an unknown state, and the handler is meant for last-moment logging and cleanup. Carrying on tends to produce silent corruption and strange failures hours later. I log, shut down gracefully with a safety timer, and exit non-zero.
Q30. Walk me through a graceful shutdown. (Must know)
On SIGTERM: set a flag so readiness returns 503, call server.close() so new connections are refused while in-flight requests finish, close idle keep-alive sockets, close the database pool and other resources when the close callback fires, and exit with 0. I add a safety timer shorter than the platform's grace period, which forces exit with 1. I tested it: a 3-second request started before the kill still returned HTTP 200 after 3.006 seconds, a new request was refused, and the process exited by itself. The bad version, which called process.exit straight away, gave the client curl: (52) Empty reply from server. See lesson 6.
Q31. How do you start a Node app in Docker so that shutdown works? (Senior signal)
I use the exec form CMD ["node", "server.js"], so Node is the main process and receives SIGTERM directly. The nodebestpractices guide warns against npm start as the entry, since npm may not pass the signal on. I also run as the non-root node user, pin the Node version, and install with npm ci --omit=dev. I did not build the image in this series because no Docker daemon was available, so I rely on the guide and the docs for the reasons.
Rapid-fire round
Answer each in one sentence, then check.
| Question | Short answer |
|---|---|
| Default thread pool size? | 4 (UV_THREADPOOL_SIZE changes it) |
process.exit or process.exitCode? | exitCode normally; exit stops at once and can lose output |
What runs first in CommonJS, nextTick or a promise then? | nextTick (in an ES module, the promise runs first) |
How do I stop a hanging fetch? | AbortSignal.timeout(ms) as the signal option |
What is port 0 in listen(0)? | Ask the OS for any free port (great for tests) |
What does server.close() do? | Stops accepting new connections and waits for existing ones to end |
Which child_process function avoids the shell? | execFile and spawn (without shell: true) |
What does Promise.race return? | The first promise to settle, success or failure |
What does --env-file do when the variable is already set in the environment? | The real environment value wins |
Why is Object.create(null) handy? | It has no prototype, so __proto__ tricks do not apply |
Common wrong answers to avoid
- "Node is single-threaded, so it can only do one thing at a time." (It waits on many things at once; it only runs JavaScript one piece at a time.)
- "Use
asyncto make a heavy function non-blocking." (It does not move work to another thread.) - "Catch the error in
uncaughtExceptionand carry on." (Log, shut down, restart.) - "
Promise.allcancels the other promises." (It does not.) - "Use
clusteralways." (Often one process per container is simpler.) - "
pipehandles errors." (It does not; usepipeline.) - "More concurrency is always faster." (It was faster only until the API said 503.)
Try it
Pick four questions you found hardest. For each one:
- Write a 10-line script that proves the answer (for example the
nextTickstarvation or theexecinjection). - Run it on your machine and compare with the numbers in this lesson.
- Explain the output to a friend in three sentences without using a term you have not defined.
Answers
There is no single answer here, but check three things. First, does your output match the pattern of mine? The exact milliseconds will differ; the ratios and the order of lines should not. Second, did you explain the "why" and not just the "what"? Third, did you mention the trap that goes with the topic?
Key takeaways
- Node runs your JavaScript on one thread, with an event loop. Waiting is cheap; calculating blocks everyone.
- Know the order: sync code,
nextTick(CommonJS), promise callbacks, timers,setImmediate. In an ES module the promise runs beforenextTick. - Errors: operational versus programmer errors, async functions turn throws into rejections, unhandled rejections crash the process by default, and
uncaughtExceptionis for logging and exit. - Use
pipeline, notpipe, and limit concurrency instead of firing everything at once. - For scale: cluster or containers for requests, worker threads for heavy tasks, a queue for long jobs, and shared state outside the process.
- For security: avoid the shell (
execFile), guard against prototype pollution and ReDoS, compare secrets withtimingSafeEqual, keep dependencies in check. - In production: structured logs, health checks, graceful shutdown tested with
curlandkill, config from the environment, and metrics for event loop delay and heap use. - In an interview, when unsure, say how you would measure it. That is a strong answer.
What's next
This is the last lesson of the series. To go further, try these:
- Build a small project that uses everything: a Fastify API with a schema, pino logs, graceful shutdown, a worker pool for one heavy route, and
node:testtests. Then profile it. - Go deeper on the server side with Backend Lesson 10 (Node.js performance basics) and Backend Lesson 11 (testing an API).
- Harden what you built with Security Lesson 1 (injection) and Security Lesson 7.
- Go back to lesson 1 (the event loop) and re-run the ordering experiment with your own variations. If you can predict the output before running it, you understand Node.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Security in Depth for Full-Stack Developers · Full-Stack Questions
- Node.js Internals Interviewers Ask About · Full-Stack Questions
- Node.js & Express Interview Questions · Full-Stack Questions
Part of the study guide
Related series