103 Node.js Interview Questions and Answers (2026)

Node.js powers a huge share of the backend world, REST and GraphQL APIs, real-time services, CLIs, and microservices all run on it. That reach means interviewers expect genuine fluency: walk in shaky on the event loop, streams, or async patterns, and you will get found out fast.
This guide gives you 103 questions with concise, interview-ready answers and code where it helps. It's worked Junior to Mid to Senior, so you build from npm and modules up to concurrency, performance, and security. Work through it and you'll have an answer for whatever they throw at you.
Q1.Explain the difference between dependencies, devDependencies, and peerDependencies.
dependencies, devDependencies, and peerDependencies.They define three kinds of dependency relationships: dependencies are needed at runtime, devDependencies are needed only during development/build, and peerDependencies declare a package your consumer must provide.
dependencies:
Required for the app to run in production (e.g. express, react).
Installed by default and shipped with your published package.
devDependencies:
Tooling only: test runners, linters, bundlers, type definitions (e.g. jest, eslint).
Installed locally but skipped with npm install --production or when your package is consumed as a dependency.
peerDependencies:
A contract: "I work with this package, but the host app must install it." Common for plugins (e.g. an ESLint plugin needing eslint).
Avoids duplicate/conflicting copies of a shared lib (like a single react instance).
Auto-installed by npm 7+, but a version mismatch produces a warning/error.
Q2.What is the purpose of package-lock.json, and why should it be committed to version control?
package-lock.json, and why should it be committed to version control?package-lock.json records the exact resolved version, location, and integrity hash of every package in your tree, so installs are deterministic. You commit it so everyone (and CI) installs byte-for-byte the same dependencies.
Locks the entire tree: Pins exact versions of direct AND transitive dependencies, not just the ranges in package.json.
Reproducibility: Without it, ^1.2.0 could resolve to a different patch/minor on each machine, causing "works on my machine" bugs.
Integrity and security: Stores an integrity hash so tampered/corrupted packages are rejected.
Why commit it:
It is the source of truth for npm ci and reviewable in PRs (you see dependency changes).
Gitignoring it defeats the purpose: builds become non-deterministic.
Q3.What is semantic versioning (SemVer), and what do the ^ and ~ symbols mean in package.json?
^ and ~ symbols mean in package.json?SemVer is the MAJOR.MINOR.PATCH versioning scheme that signals the kind of change in a release; ^ and ~ are range operators that control how far npm may auto-upgrade.
The three numbers:
MAJOR: breaking, incompatible API changes.
MINOR: new features, backward compatible.
PATCH: backward-compatible bug fixes.
Caret ^: Allows changes that do not modify the leftmost non-zero digit: ^1.2.3 matches >=1.2.3 <2.0.0 (minor + patch).
Tilde ~: Allows patch-level changes only: ~1.2.3 matches >=1.2.3 <1.3.0.
Notes:
For 0.x versions ^ is stricter (0.x treated as potentially breaking).
An exact pin uses no operator (1.2.3); ranges are why the lockfile matters.
Q4.What is the difference between package-lock.json and package.json?
package-lock.json and package.json?package.json is the human-authored manifest of intent (version ranges, scripts, metadata), while package-lock.json is the machine-generated record of the exact tree that was actually installed.
package.json:
Edited by hand; declares dependency ranges (^4.18.0), scripts, name, version, config.
Lists only direct dependencies.
package-lock.json:
Auto-generated; pins exact resolved versions plus integrity hashes.
Captures the full tree including transitive dependencies.
Relationship:
package.json says "what I want," the lockfile says "exactly what I got."
You edit the manifest; npm updates the lock to satisfy it.
Q5.What is the purpose of npx, and how does it differ from npm?
npx, and how does it differ from npm?npx is a package runner: it executes a package's binary without permanently installing it, whereas npm is the package manager that installs and manages dependencies.
What npx does:
Runs a CLI from a local node_modules/.bin if present, otherwise downloads it to a temp cache and runs it.
Great for one-off tools: npx create-react-app my-app without a global install.
How it differs from npm:
npm install puts a package on disk; npx executes one.
Avoids polluting global space and avoids version drift (always run a fresh/specified version).
Bonus: Can pin a version: npx cowsay@2; ships with npm 5.2+.
Q6.What is the difference between npm install and npm ci?
npm install and npm ci?npm install resolves and updates dependencies (and may modify the lockfile), while npm ci does a clean, exact install straight from the lockfile, ideal for CI and reproducible builds.
npm install:
Reads package.json ranges, can add/upgrade packages, and writes changes back to package-lock.json.
Installs incrementally on top of existing node_modules.
npm ci:
Requires a lockfile and installs exactly what it specifies, never modifying it.
Deletes node_modules first for a clean, deterministic install.
Fails fast if package.json and the lockfile are out of sync.
Rule of thumb: Use npm install while developing; use npm ci in CI/CD and Docker builds.
Q7.What are npm scripts, and how do you use the scripts field in package.json?
npm scripts, and how do you use the scripts field in package.json?npm scripts are command shortcuts defined in the scripts field of package.json; they let you run build, test, and tooling commands consistently with npm run <name>.
How they work:
Each key is a name and each value is a shell command run via npm run <name>.
They automatically add node_modules/.bin to PATH, so local CLIs run without a global install.
Lifecycle / reserved names:
start and test can be run without run (npm start).
pre and post hooks auto-run around a script (e.g. prebuild before build).
Composition: Chain scripts with && or call others via npm run; pass extra args after --.
Q8.Explain the difference between __dirname and process.cwd().
__dirname and process.cwd().`__dirname` is the absolute path of the directory containing the current module file, fixed at the file's location; `process.cwd()` is the working directory the Node process was launched from, which can vary by how/where you run it.
`__dirname`:
Tied to the file itself, so it's stable no matter where you start the process.
Use it to resolve files relative to your code (templates, assets, sibling modules).
In CommonJS it's built in; in ES modules use `import.meta.dirname` (or derive from `import.meta.url`).
`process.cwd()`:
The current working directory of the running process, changeable via `process.chdir()`.
Use it for user-facing paths relative to where the command was run (CLI tools, config lookup).
Key contrast: `__dirname` answers "where is this file?"; `process.cwd()` answers "where was Node started?"
Q9.What are exit codes in Node.js, and why is process.exit(1) used?
process.exit(1) used?An exit code is an integer a process returns to the OS when it terminates: `0` signals success and any non-zero value signals failure. `process.exit(1)` forces an immediate exit with a generic error code, telling callers (shells, CI, orchestrators) that something went wrong.
Convention:
`0` = success, non-zero = error; the specific number can encode a failure category.
Node sets the code automatically (e.g. `1` for an uncaught exception).
Why use `process.exit(1)`:
To deliberately fail a script so CI pipelines and shell scripts detect the error.
Prefer setting `process.exitCode = 1` and letting the event loop drain instead of calling `process.exit()`.
Caution: `process.exit()` terminates immediately and may truncate pending I/O (unflushed `stdout`, in-flight writes).
Q10.When would you use the path module instead of simple string concatenation for file paths?
path module instead of simple string concatenation for file paths?Use the `path` module whenever you build or manipulate file paths, because it handles OS differences (separators), normalizes redundant segments, and resolves relative paths correctly, which naive string concatenation gets wrong.
Cross-platform separators: `path.join()` uses `/` on POSIX and `\` on Windows, so the same code works everywhere.
Normalization and safety:
It collapses duplicate slashes and resolves `..`/`.` segments, avoiding malformed paths.
`path.resolve()` produces an absolute path from segments.
Parsing helpers: `path.basename()`, `path.dirname()`, `path.extname()` extract parts reliably.
Rule of thumb: Concatenation like `dir + '/' + file` is brittle; prefer `path.join(dir, file)`.
Q11.How do you manage configuration and secrets using environment variables and process.env in Node.js?
process.env in Node.js?Configuration and secrets are kept out of source code and injected through environment variables, read in Node via `process.env`. This follows the twelve-factor principle of separating config from code so the same build runs in any environment.
Reading config:
`process.env.VAR_NAME` returns a string (or `undefined`); coerce types and validate at startup.
Fail fast: throw if a required secret is missing rather than running half-configured.
Local development: Use a `.env` file with `dotenv` (or Node's built-in `--env-file`), and never commit it (add to `.gitignore`).
Production secrets: Inject via the platform's secret manager (Kubernetes secrets, Vault, cloud parameter stores), not files in the repo.
Good practice: Centralize access in one config module that validates with a schema (e.g. `zod`/`joi`) so the rest of the app reads typed values.
Q12.What are 'Global Objects' in Node.js, and how do they differ from the window object in a browser?
window object in a browser?Global objects in Node are values available in every module without requiring/importing them. Unlike the browser's `window`, there is no shared browser DOM/global namespace; Node's true global is `globalThis`, and many "globals" are actually module-scoped variables, not real global properties.
Real globals: `process`, `console`, `Buffer`, `globalThis`, timer functions like `setTimeout`/`setInterval`.
Module-scoped "pseudo-globals": `__dirname`, `__filename`, `require`, `module`, `exports` exist per-CommonJS-module, not on `globalThis`.
Differences from `window`:
No DOM, no `document`, no `window`; browser-only APIs aren't present.
Browsers expose `window` as the global; Node standardizes on `globalThis`.
Node provides server-side globals (`process`, `Buffer`) a browser lacks.
Q13.What is the difference between global and globalThis in Node.js, and what globals are available without requiring a module?
global and globalThis in Node.js, and what globals are available without requiring a module?In Node.js global is the Node-specific global object, while globalThis is a standardized cross-environment reference to the same object; they point to the same thing in Node but globalThis is portable code that also works in browsers and workers.
global (Node-only): Historically the way to reach the global scope in Node; equivalent to window in a browser but with a different name.
globalThis (standardized in ES2020): Resolves to the global object regardless of environment, so prefer it for portable libraries.
Globals available without require/import:
Timers: setTimeout, setInterval, setImmediate, and queueMicrotask.
Process and console: process, console.
Module context vars (in CommonJS): __dirname, __filename, require, module, exports (these are actually module-scoped, not true globals).
Web-standard APIs now exposed globally: Buffer, URL, fetch, TextEncoder.
Q14.What is the Node.js REPL, and what is it useful for?
REPL, and what is it useful for?The REPL (Read-Eval-Print Loop) is Node's interactive shell: you type JavaScript, it evaluates it immediately and prints the result, making it ideal for quick experiments and debugging.
How it works: Reads each line, evaluates it, prints the result, then loops; start it by running node with no arguments.
What it's useful for:
Trying out a language feature or an API without writing a file.
Inspecting return values and exploring loaded modules interactively.
Quick debugging or prototyping of a snippet.
Handy features:
The _ variable holds the last result; commands like .help, .save, and .load control the session.
Awaiting top-level promises and tab-completion are supported.
Q15.What is a Buffer in Node.js, and why is it necessary for handling binary data?
Buffer in Node.js, and why is it necessary for handling binary data?A Buffer is a fixed-length chunk of raw memory outside the V8 heap for storing binary data. It's necessary because JavaScript strings are UTF-16 and can't faithfully represent arbitrary bytes like file or network payloads.
What it is:
A subclass of Uint8Array representing a sequence of bytes (0–255).
Allocated outside the V8 heap, so large binary data doesn't pressure GC.
Why it's needed:
I/O is inherently binary: TCP sockets, files, and crypto deal in raw bytes.
Lets you control encodings explicitly when converting to/from strings.
Working with it:
Create with Buffer.from() or Buffer.alloc(); convert with .toString(encoding).
Use Buffer.alloc() over the unsafe allocUnsafe() unless you immediately overwrite it.
Q16.What is the difference between a Buffer and a String in Node.js?
Buffer and a String in Node.js?A Buffer is a fixed-length chunk of raw binary data (bytes) outside the V8 heap, while a String is an immutable sequence of characters that V8 stores with a specific text encoding.
Buffer: raw bytes:
Holds binary data exactly as-is (file contents, network packets, crypto bytes).
Fixed size once allocated, and lives off-heap for efficient I/O.
String: decoded text:
Immutable; any "edit" creates a new string.
Always tied to an encoding when converting to/from bytes.
Conversion needs an encoding: Use buf.toString('utf8') and Buffer.from(str, 'utf8'); a wrong encoding corrupts the data.
Why it matters: Multibyte characters (UTF-8) can split across buffer chunks, so never decode chunk-by-chunk naively; use StringDecoder or buffer fully first.
Q17.What are Node.js Streams, and why are they preferred over fs.readFile for large files?
Streams, and why are they preferred over fs.readFile for large files?Streams are an abstraction for processing data in small chunks over time rather than loading it all at once, so they keep memory flat and start producing output immediately, unlike fs.readFile which buffers the whole file in memory.
What streams are:
EventEmitter-based interfaces for incremental data: Readable, Writable, Duplex, Transform.
Data arrives as chunks you handle as they come.
Why preferred for large files:
Constant memory: a 2 GB file streams in small chunks instead of allocating 2 GB.
Lower latency: processing/responding begins on the first chunk, not after the full read.
Composable: pipe through transforms (gzip, encryption) without intermediate copies.
The fs.readFile tradeoff: Simple for small files, but a large file can exhaust memory or hit the buffer size limit and blocks responsiveness until done.
Q18.What is the significance of the .mjs and .cjs file extensions?
.mjs and .cjs file extensions?They are unambiguous, override-proof signals of module format: .mjs forces ES Module parsing and .cjs forces CommonJS, regardless of any package.json "type" setting.
.mjs is always ESM: Uses import/export, supports top-level await, and has no require or __dirname by default.
.cjs is always CommonJS: Uses require/module.exports even inside an ESM package.
Why they matter: They let you mix both systems in one project: e.g. ship a .cjs config file inside a "type": "module" package.
Q19.What is the difference between exports and module.exports?
exports and module.exports?In CommonJS, exports starts as just a reference to module.exports; what actually gets exported is module.exports, so reassigning exports alone breaks the link and exports nothing.
They point to the same object initially: Node effectively does exports = module.exports = {}, so adding properties to either works the same.
Adding properties is safe on both: exports.foo = ... and module.exports.foo = ... are equivalent.
Reassignment differs:
exports = something only changes the local variable; the module still exports the original module.exports.
To export a single value (function, class), assign to module.exports directly.
Q20.How does middleware work in Express, and what is the purpose of the next() function?
Express, and what is the purpose of the next() function?Express middleware are functions that run in sequence for each request, each receiving (req, res, next); next() passes control to the next middleware in the stack, and without calling it the request hangs.
The pipeline model: Middleware registered with app.use() or route methods run in registration order, sharing the same req/res.
What each can do: Modify req/res, end the response with res.send(), or call next() to continue.
The role of next():
next() advances the chain; next(err) skips ahead to error-handling middleware.
Forgetting it (without sending a response) leaves the request stuck.
Error-handling middleware: Defined with four args (err, req, res, next); Express routes errors here.
Q21.How do you create a basic HTTP server using Node's built-in http module without Express?
http module without Express?Use the core http module's http.createServer(), which takes a request listener (req, res) and returns a server you start with .listen().
Create the server: The callback fires for every request; req is a readable stream, res is a writable stream.
Set status and headers: Use res.writeHead(status, headers) or res.statusCode before writing the body.
End the response: You must call res.end() or the connection hangs; routing is manual via req.url and req.method.
Q22.What is the difference between query parameters, route parameters, and the request body in an Express route?
Express route?They are three different ways a client sends data: route parameters are part of the URL path, query parameters are the key-value pairs after the ?, and the request body is the payload sent in the message body (typically with POST/PUT).
Route parameters:
Named path segments accessed via req.params: /users/:id gives req.params.id.
Used to identify a specific resource.
Query parameters:
Accessed via req.query: /users?sort=asc gives req.query.sort.
Used for optional filtering, sorting, pagination.
Request body:
Accessed via req.body, but only after a body-parsing middleware like express.json() runs.
Used to send larger or structured payloads for creating/updating resources.
Q23.How do you parse incoming JSON or form data in an Express application?
Express application?Use Express's built-in body-parsing middleware: express.json() for JSON payloads and express.urlencoded() for HTML form submissions. They read the request stream and populate req.body.
JSON bodies: express.json() parses requests with Content-Type: application/json into a JS object.
Form bodies: express.urlencoded({ extended: true }) parses application/x-www-form-urlencoded; extended: true allows nested objects via the qs library.
Order matters: Register these with app.use() before your routes, or req.body will be undefined.
Other types: For file uploads (multipart/form-data) use a library like multer; these parsers don't handle it.
Q24.What is the EventEmitter class, and how does Node.js use it to implement the observer pattern?
EventEmitter class, and how does Node.js use it to implement the observer pattern?EventEmitter is a core Node.js class that implements the observer (publish/subscribe) pattern: objects emit named events and any number of registered listener functions are invoked synchronously when those events fire.
The core API:
on(event, listener) subscribes; emit(event, ...args) publishes; once subscribes for a single fire.
off/removeListener unsubscribes.
Maps to the observer pattern: The emitter is the subject; listeners are observers, decoupled from each other and from the emitter.
It underpins much of Node's API: Streams, http.Server, sockets, and process all extend EventEmitter (e.g. server.on('request', ...)).
Listeners run synchronously: emit calls each listener in registration order, in the same tick, not asynchronously.
Q25.What is the 'error-first callback' pattern, and why was it the standard in early Node.js?
The error-first callback pattern is a convention where an async function's callback receives the error as its first argument and the result(s) afterward: callback(err, data). It was the standard before promises because it gave the whole ecosystem one predictable way to surface async errors.
The convention:
If the operation failed, the first arg is an Error and the rest are undefined.
If it succeeded, the first arg is null and the result follows.
Why it became the standard:
Async errors can't be caught with try/catch across event-loop boundaries, so the error had to travel through the callback.
A single agreed shape let libraries interoperate and tools like util.promisify automate conversion.
You must check the error first: Forgetting to handle err silently swallows failures; the idiom is to return early on error.
Largely superseded: Promises and async/await now dominate, but many core APIs still expose callback forms.
Q26.Why should you avoid using synchronous versions of functions (like fs.readFileSync) in a web server?
fs.readFileSync) in a web server?Synchronous functions block the single event-loop thread until they finish, so during a sync file read or hash the server cannot process any other request: throughput collapses under concurrency. In a web server you should use the async (non-blocking) variants instead.
Node has one event-loop thread: A blocking call like fs.readFileSync halts the loop, so all concurrent clients wait, not just the current one.
Concurrency is the whole point of Node: Async I/O lets one thread handle thousands of in-flight requests by yielding during I/O; sync calls throw that away.
Symptoms: Latency spikes, dropped throughput, and unresponsive health checks under load.
When sync is acceptable: One-time startup work (reading config before the server listens) or CLI scripts, where nothing else is being served.
Prefer async APIs: Use fs.promises.readFile with await, and offload heavy CPU work to worker threads.
Q27.What is 'callback hell', and what techniques does Node.js offer to avoid it?
Node.js offer to avoid it?"Callback hell" (also called the "pyramid of doom") is the deeply nested, hard-to-read code that results from chaining many asynchronous operations using callbacks, where each step nests inside the previous one's callback.
Why it happens: Each async call takes a callback, and sequencing dependent operations nests them, growing rightward indentation and making error handling repetitive and brittle.
Promises: Flatten nesting via .then() chaining and centralize errors with a single .catch().
async/await: Lets async code read top-to-bottom like synchronous code, using try/catch for errors.
Modularization: Extract named functions instead of inline anonymous callbacks to keep nesting shallow.
Utility helpers: Use util.promisify() to convert callback-style APIs to promises, or Promise.all() for concurrent operations.
Q28.What does npm audit do, and how do you handle vulnerabilities in your dependency tree?
npm audit do, and how do you handle vulnerabilities in your dependency tree?npm audit scans your installed dependency tree against a vulnerability database and reports known security issues by severity, with suggested fixes.
What it reports: Affected package, severity (low/moderate/high/critical), the vulnerable path, and a fixed version if one exists.
How to fix:
npm audit fix: auto-upgrades to compatible patched versions within your ranges.
npm audit fix --force: applies breaking upgrades (use cautiously, can break the build).
Manually bump the offending direct dependency, or use overrides to force a transitive dependency to a safe version.
Judgment matters:
Assess real exposure: a dev-only or unreachable vuln may not be exploitable.
Sometimes no fix exists yet: pin, patch, or wait on the maintainer.
In CI: Gate builds with npm audit --audit-level=high to fail only on serious issues and reduce noise.
Q29.Is Node.js truly single-threaded? If so, how does it handle concurrent I/O operations?
Node.js truly single-threaded? If so, how does it handle concurrent I/O operations?Node.js runs your JavaScript on a single main thread (the event loop), but it is not single-threaded as a whole: I/O is delegated to libuv, which uses the OS and a thread pool to run operations concurrently while your code keeps executing.
One JS thread, the event loop: Your JavaScript callbacks run one at a time, so there are no data races in user code.
I/O is offloaded, not blocking:
Network sockets use the OS's async mechanisms (epoll, kqueue, IOCP); Node registers a callback and moves on.
File system and DNS work that has no async OS API runs on libuv's thread pool (default 4 threads).
Completion via the event loop: When an operation finishes, its callback is queued and run when the stack is clear.
The caveat: CPU-bound work on the main thread blocks everything; use worker_threads or separate processes for that.
Q30.Why is Node.js often preferred for I/O-intensive applications but not for heavy data processing?
Node.js often preferred for I/O-intensive applications but not for heavy data processing?Node excels at I/O-bound work because its single thread can juggle many waiting operations without blocking, but heavy CPU-bound processing runs on that same thread and stalls the event loop, freezing every other request.
Great for I/O: While one request waits on a DB, network, or disk, the loop serves others, so throughput stays high with minimal resources.
Bad for heavy CPU: A long computation (image processing, big JSON crunching, cryptography loops) monopolizes the single JS thread; all pending callbacks wait.
Why the asymmetry: I/O is delegated to libuv/OS, but JS computation cannot be; it has nowhere to yield.
Mitigations: Use worker_threads, child processes, a job queue, or a service in another language for CPU-heavy tasks.
Q31.Explain the difference between NODE_ENV=development and NODE_ENV=production in terms of runtime behavior.
NODE_ENV=development and NODE_ENV=production in terms of runtime behavior.NODE_ENV is a convention, not a Node built-in: many libraries read it to switch behavior, with production enabling caching and lean output and development enabling verbose debugging aids.
It's a signal libraries respect: Node core mostly ignores it; frameworks like Express and tools like Webpack key off it.
In production:
Express caches view templates and shows generic error pages (no stack traces to clients).
Build tools minify, drop dev warnings, and optimize; some packages skip expensive checks.
In development: More logging, detailed errors and stack traces, no template caching for faster iteration.
Practical note: Set it explicitly in deployment; forgetting production can cost real performance and leak error details.
Q32.What is the process object, and how would you use it to handle graceful shutdowns?
process object, and how would you use it to handle graceful shutdowns?process is a global object representing the running Node process: its environment, lifecycle, and I/O streams. For graceful shutdown you listen for termination signals, stop accepting new work, finish in-flight requests, close resources, then exit.
What it gives you for shutdown:
Signal events like SIGTERM and SIGINT (Ctrl+C, orchestrator stop).
process.exit(code) to terminate, and process.exitCode to set the code without forcing an immediate exit.
Graceful shutdown steps:
Stop accepting new connections (server.close()).
Let in-flight requests finish, then close DB pools and other resources.
Exit; add a timeout to force-exit if cleanup hangs.
Also guard against crashes: Handle uncaughtException and unhandledRejection to log and shut down cleanly.
Q33.What is the process object, and what are some of its most useful properties for a production app?
process object, and what are some of its most useful properties for a production app?process is the global object exposing information about and control over the current Node process. A handful of its members are essential for configuring, observing, and safely running a production app.
Configuration:
process.env: environment variables (secrets, NODE_ENV, ports, feature flags).
process.argv: command-line arguments for CLI tooling.
Lifecycle and signals:
process.on('SIGTERM' / 'SIGINT') for graceful shutdown; process.exit()/process.exitCode.
process.on('uncaughtException') and 'unhandledRejection' for last-resort error handling.
Observability and health:
process.memoryUsage() and process.cpuUsage() for metrics and leak detection.
process.uptime() and process.pid for health checks and logging.
Runtime info: process.version/process.versions (Node and V8 versions) and process.platform for environment-specific logic.
Async scheduling: process.nextTick(): defer a callback to run before the next event loop phase.
Q34.What does util.promisify do, and when would you use it?
util.promisify do, and when would you use it?`util.promisify` converts a callback-based function that follows Node's error-first convention (`(err, result) => ...`) into one that returns a Promise, so you can `await` it instead of nesting callbacks.
What it expects:
The original function's last argument must be a callback whose first param is the error.
It returns a new function giving a Promise that resolves with the result or rejects with the error.
When to use it:
Wrapping legacy or third-party callback APIs to use `async`/`await`.
When no native Promise version exists (many core modules now offer one, e.g. `fs/promises`).
Caveat: It only works for the error-first signature; non-standard callbacks need manual wrapping (or a custom `util.promisify.custom`).
Q35.Explain the phases of the Node.js event loop. Which phase handles setImmediate vs. setTimeout?
setImmediate vs. setTimeout?The event loop processes callbacks in a fixed sequence of phases, each with its own queue; setTimeout callbacks run in the timers phase while setImmediate callbacks run in the dedicated check phase.
Timers: Executes callbacks scheduled by setTimeout and setInterval whose threshold has elapsed.
Pending callbacks: Runs certain deferred system callbacks, like some TCP errors.
Idle/prepare: Internal use only.
Poll: Retrieves new I/O events and executes their callbacks; the loop may block here waiting for I/O.
Check: Executes setImmediate callbacks.
Close callbacks: Handles close events like socket.on('close').
Note: between each phase (and between callbacks), Node drains the microtask queues (process.nextTick first, then promises). Order of setTimeout(fn, 0) vs setImmediate at the top level is non-deterministic, but inside an I/O callback setImmediate always fires first.
Q36.Explain the difference between process.nextTick() and setImmediate(). When would you use one over the other?
process.nextTick() and setImmediate(). When would you use one over the other?Both defer work, but process.nextTick() runs before the event loop continues (after the current operation, before any I/O or timers), whereas setImmediate() runs on the next loop iteration in the check phase.
process.nextTick():
Its callback is placed on the microtask-like nextTick queue, drained immediately after the current operation completes and before the loop proceeds.
Use it to let the current function finish before running a callback, or to defer an error emission until listeners are attached.
setImmediate():
Its callback runs in the check phase of the next iteration, so it yields to pending I/O first.
Use it to break up long-running work and give the loop a chance to handle I/O.
Rule of thumb: Prefer setImmediate() when you want to yield; reserve process.nextTick() for must-run-before-anything-else cases, since overuse can starve the loop.
Q37.What is the 'Thread Pool' in Node.js, and which specific types of tasks are offloaded to it?
The thread pool is a set of background worker threads provided by libuv (default size 4) that Node uses to run certain operations that have no async OS primitive, so the main event loop thread stays free.
Why it exists: Node is single-threaded for JS, but some work is inherently blocking; libuv offloads it to pool threads and posts the result back via a callback.
What gets offloaded:
File system operations in the fs module (most of them).
DNS lookups via dns.lookup().
Some crypto functions like crypto.pbkdf2() and crypto.randomBytes().
Compression in zlib.
What does NOT use it: Network I/O (TCP/HTTP) uses the OS's async mechanisms (epoll/kqueue/IOCP), not the thread pool.
Tuning: Size is configurable via UV_THREADPOOL_SIZE; too few threads for heavy crypto/fs can cause queuing.
Q38.What happens if you block the event loop? Give an example of a common operation that might do this.
Blocking the event loop means running synchronous CPU-heavy or blocking code on the main thread, which prevents Node from processing any other callbacks: all incoming requests, timers, and I/O callbacks stall until the operation finishes.
Consequence: Because JS runs on one thread, a single slow operation freezes the entire server, hurting throughput and latency for every client.
Common culprits:
Synchronous file APIs like fs.readFileSync on large files.
Large JSON.parse/JSON.stringify, giant loops, or complex regex (ReDoS).
Heavy synchronous crypto or compression.
How to avoid: Use async APIs, offload CPU work to worker_threads or a child process, and chunk long loops with setImmediate.
Q39.What is the difference between the microtask queue and the macrotask queue in Node.js?
Microtasks are high-priority callbacks drained completely after each operation and between every macrotask, while macrotasks are the larger units of work scheduled into the event loop's phases; microtasks always run before the next macrotask.
Microtask queue:
Includes resolved Promise callbacks (.then/await) and queueMicrotask.
In Node, process.nextTick callbacks run in their own queue that is drained even before the promise microtasks.
Drained fully (including newly added ones) before the loop moves on.
Macrotask queue:
Includes setTimeout, setInterval, setImmediate, and I/O callbacks, each tied to a specific event loop phase.
Only one macrotask runs per turn, then microtasks drain again.
Practical takeaway: A flood of microtasks (or recursive nextTick) can starve macrotasks and delay timers/I/O.
Q40.When would you use setTimeout(fn, 0) vs. setImmediate(fn)?
setTimeout(fn, 0) vs. setImmediate(fn)?Both defer work, but setImmediate() runs in the check phase right after I/O, while setTimeout(fn, 0) runs in the timers phase after a minimum (~1ms) delay. Inside an I/O callback their order is deterministic; at the top level it isn't.
Use setImmediate() when:
You want to run after the current I/O operation completes but before any timers.
Inside an I/O callback it always fires before setTimeout(fn, 0), making it predictable.
Use setTimeout(fn, 0) when: You want minimal scheduled delay and don't depend on the exact phase ordering.
Top-level caveat: In the main module their order is non-deterministic because it depends on how long the process took to reach the loop.
Q41.Explain the difference between Readable, Writable, Duplex, and Transform streams.
Readable, Writable, Duplex, and Transform streams.These are the four stream types defined by data direction: Readable produces data, Writable consumes it, Duplex does both independently, and Transform is a duplex stream where output is derived from input.
Readable: A source you read from: fs.createReadStream, an HTTP request body.
Writable: A sink you write to: fs.createWriteStream, an HTTP response.
Duplex: Both readable and writable with two independent channels: a TCP net.Socket.
Transform: A duplex stream where the read side is a function of the write side: zlib.createGzip or a crypto cipher.
Q42.What is the purpose of the .pipe() method, and what are its limitations compared to pipeline()?
.pipe() method, and what are its limitations compared to pipeline()?The .pipe() method connects a readable stream to a writable stream and automatically manages data flow and backpressure, but it does not reliably clean up or propagate errors, which is why pipeline() exists.
What .pipe() does:
Reads from source and writes to destination, pausing the source when the destination is full (backpressure).
Returns the destination so you can chain: a.pipe(b).pipe(c).
Its limitations:
Errors are NOT forwarded: an error on the source won't destroy the destination, leaking file descriptors.
You must attach 'error' handlers to every stream manually.
Why pipeline() is preferred: It pipes all streams, propagates errors, destroys every stream on failure, and gives one completion callback (or a Promise via stream/promises).
Q43.When would you use pipe() versus manually handling stream events like data, end, and error?
pipe() versus manually handling stream events like data, end, and error?Use pipe() (or pipeline()) when you just need to move data from a source to a destination with automatic backpressure; handle data/end/error manually only when you need custom per-chunk logic that a transform can't express simply.
Prefer pipe()/pipeline():
It handles backpressure for you (pausing the source automatically).
Less boilerplate and fewer bugs; pipeline() also cleans up on errors.
Handle events manually when:
You need fine control: inspecting/aggregating chunks, conditional routing, or non-stream destinations.
You must implement custom backpressure: respect write() returning false and wait for 'drain'.
Caveat with manual handling: Easy to forget backpressure or error cleanup; often a Transform stream piped in is cleaner than raw event handling.
Q44.Explain the difference between a Duplex stream and a Transform stream.
Duplex stream and a Transform stream.Both can be read from and written to, but a Duplex stream's read and write sides are independent channels, whereas a Transform stream is a Duplex whose output is computed from its input.
Duplex: two unrelated sides:
What you write and what you read are not connected by definition (e.g. a TCP socket: sending and receiving are separate).
You implement _read() and _write() separately.
Transform: output derived from input:
A subclass of Duplex where each written chunk is processed into readable output (e.g. zlib.createGzip(), a cipher).
You implement _transform() (and optionally _flush()).
Rule of thumb: If output is a function of input, use Transform; if the two directions are logically distinct, use Duplex.
Q45.What is the purpose of the highWaterMark option in a stream?
highWaterMark option in a stream?The highWaterMark sets the internal buffer threshold (in bytes, or in objects for object mode) that tells a stream when it's "full," which is the basis for backpressure.
For readable streams: How much data to buffer internally before it stops pulling from the source.
For writable streams: When buffered data reaches it, write() returns false, signaling the producer to pause until 'drain'.
Defaults and tuning:
Default is 16 KB for byte streams, 16 objects in object mode.
Higher value: more throughput but more memory; lower: less memory but more overhead.
Key point: It's a threshold/hint, not a hard cap; oversized chunks can still exceed it.
Q46.What happens internally when you call .pipe() on a readable stream?
.pipe() on a readable stream?Calling .pipe() wires the readable's data events to the writable's write(), switches the readable into flowing mode, and sets up automatic backpressure and end-of-stream handling.
It subscribes to source events: On each 'data' chunk it calls dest.write(chunk).
It manages backpressure: If write() returns false, it calls source.pause(), then source.resume() on the destination's 'drain'.
It handles completion: On source 'end' it calls dest.end() (unless { end: false }).
What it does NOT do: It does not forward errors or destroy streams on failure, so you still need explicit error handling or pipeline().
Q47.Explain the difference between 'flowing' and 'paused' modes in readable streams.
'flowing' and 'paused' modes in readable streams.A readable stream is in paused mode by default and you must pull data explicitly; in flowing mode data is pushed to you automatically via 'data' events as fast as it arrives.
Paused mode (default):
You call read() to pull chunks on demand, giving you control over pacing.
Triggered/kept by not consuming, or by calling pause().
Flowing mode: Chunks are emitted as 'data' events automatically; you must keep up or risk memory growth.
What switches modes:
To flowing: adding a 'data' listener, calling resume(), or calling pipe().
To paused: pause(), or unpipe() with no remaining destinations.
Why it matters: Using pipe() or for await...of handles the mode and backpressure for you, avoiding lost data or memory blowups.
Q48.How do streams help in reducing the memory footprint of a Node.js application?
Streams process data in small chunks as it flows, so you never hold the entire payload in memory at once: a 2GB file moves through a few kilobytes at a time instead of being fully buffered.
Chunked processing instead of whole-buffer: Reading a file with fs.readFile loads it all into RAM; fs.createReadStream emits manageable chunks.
Backpressure keeps memory bounded:
When a slow consumer can't keep up, .write() returns false and the source pauses, so data doesn't pile up in buffers.
pipe() and pipeline() handle this automatically.
Constant memory regardless of size: Memory usage stays roughly flat whether the file is 10MB or 10GB, since only one chunk plus the high-water-mark buffer is resident.
Composable pipelines: Chaining transforms (read, gzip, write) streams data end to end without intermediate full copies.
Q49.What is the difference between Buffer.alloc() and Buffer.allocUnsafe()?
Buffer.alloc() and Buffer.allocUnsafe()?Both allocate a Buffer of a given size, but Buffer.alloc() zero-fills the memory while Buffer.allocUnsafe() skips initialization for speed, leaving whatever bytes were already in that memory.
Buffer.alloc(size):
Returns a buffer filled with zeros (safe), slightly slower because of the fill step.
Use it by default, especially when the buffer may be sent, logged, or stored before being fully overwritten.
Buffer.allocUnsafe(size):
Returns uninitialized memory that may contain old data (potentially sensitive), faster because no zeroing happens.
Only safe when you immediately and completely overwrite every byte.
Security implication: Leftover bytes from allocUnsafe() could leak previous memory contents (passwords, keys) if exposed without overwriting.
Rule of thumb: Prefer alloc(); reach for allocUnsafe() only in hot paths where you control the full fill.
Q50.What does the zlib module do, and when would you use compression in a Node.js application?
zlib module do, and when would you use compression in a Node.js application?The zlib module provides compression and decompression (gzip, deflate, brotli) built on the zlib/brotli libraries, exposed as both stream and one-shot APIs. You use it to shrink data for faster network transfer or smaller storage.
What it offers:
Algorithms: createGzip(), createDeflate(), createBrotliCompress() and their decompress counterparts.
Stream form for piping large data, and sync/async one-shot form (gzipSync, gunzip) for buffers.
When to compress:
HTTP responses: send Content-Encoding: gzip to reduce bandwidth for text, JSON, HTML.
Files and logs: compress at rest to save disk.
Inter-service payloads where network is the bottleneck.
When not to:
Already-compressed data (images, video, zip) gains little and wastes CPU.
Tiny payloads where overhead outweighs savings.
Tradeoff: Compression trades CPU for smaller size; pick a level (e.g. brotli quality) that balances both.
Q51.What are the fundamental differences between CommonJS (require) and ES Modules (import) in Node.js?
require) and ES Modules (import) in Node.js?CommonJS (require) is Node's original synchronous module system, while ES Modules (import) are the standardized, statically analyzable, asynchronous system shared with browsers. They differ in loading, syntax, timing, and bindings.
Loading model:
CommonJS resolves and executes synchronously at runtime, so require() can be called conditionally anywhere.
ESM is parsed first (static): imports are resolved before execution, enabling tree-shaking and top-level await.
Syntax and bindings: CJS uses module.exports/require and copies values; ESM uses export/import with live bindings.
Available globals: CJS has __dirname, __filename, require; ESM does not, but offers import.meta.url.
How Node picks the mode: By .mjs/.cjs extension or "type": "module" in package.json.
Strict mode: ESM is always in strict mode; CJS is not by default.
Q52.What is the 'module wrapper' in Node.js, and how does it provide variables like __dirname and __filename?
__dirname and __filename?Before running a CommonJS file, Node wraps its source in a function so the module gets its own scope and is handed key variables as function arguments. This wrapper is how __dirname, __filename, require, module, and exports appear without being globals.
What the wrapper looks like: Node effectively compiles your code as (function (exports, require, module, __filename, __dirname) { ... }).
Why it exists:
Encapsulation: top-level variables stay local to the module instead of leaking to the global scope.
Injection: it provides per-module helpers without polluting the global object.
Where the values come from: Node computes __filename from the resolved module path and __dirname as its directory, then passes them in as arguments.
Note for ESM: ES Modules are not wrapped this way, so __dirname is absent; derive it from import.meta.url.
Q53.Can you require an ESM module in a CommonJS file? Why or why not?
require an ESM module in a CommonJS file? Why or why not?You cannot directly require() an ES Module, because ESM loading is asynchronous while require() is synchronous; instead you use the async dynamic import(). (Recent Node versions can synchronously require fully-synchronous ESM, but the general rule still holds.)
Why require() fails:
ESM may use top-level await and is resolved/evaluated asynchronously, which a synchronous require() can't represent.
Traditionally throws ERR_REQUIRE_ESM.
The supported way: Use dynamic import(), which returns a promise and works from CommonJS.
Recent Node nuance: Node 22+ allows require() of ESM that has no top-level await (synchronous graph), but relying on it reduces portability.
Q54.How does Node.js decide whether to treat a .js file as CommonJS or an ES Module?
.js file as CommonJS or an ES Module?Node.js decides per-file by checking the nearest package.json "type" field and the file extension: a .js file is ESM only when the closest package's "type": "module", otherwise CommonJS.
Extension wins first: .mjs is always ESM; .cjs is always CommonJS, regardless of package.json.
For ambiguous .js, the nearest package.json decides: "type": "module" treats .js as ESM; "type": "commonjs" or no field defaults to CommonJS.
Recent Node also supports require of ESM: Node can detect ESM syntax and load it, but the format decision still follows extension and "type".
Practical takeaway: Be explicit: set "type" in package.json or use .mjs/.cjs to avoid surprises.
Q55.Explain the concept of 'Top-level await' and its availability in different Node.js module systems.
await' and its availability in different Node.js module systems.Top-level await lets you use await directly at a module's top level (outside any async function), and it is available only in ES Modules, not in CommonJS.
What it does: Pauses the module's evaluation until the awaited promise resolves; importers wait for that module to finish before running.
Where it works:
ESM only: .mjs files or .js under "type": "module".
Not in CommonJS, because require is synchronous and can't await an async module.
Common uses: Awaiting dynamic import(), config loading, or DB connections before exporting.
Caveat: It can delay the whole import graph, so don't block startup on slow operations unnecessarily.
Q56.How does Node.js resolve a module path when you call require()? Walk through the module resolution algorithm.
require()? Walk through the module resolution algorithm.When you call require(x), Node resolves x to an absolute file path by checking, in order: core modules, then relative/absolute paths, then node_modules folders walking up the directory tree.
Core modules first: Names like fs or path (and node: prefixed) resolve immediately and stop the search.
Relative/absolute paths (./, ../, /):
Try the exact file, then with extensions .js, .json, .node.
If it's a directory, read its package.json "main" (or "exports"), else fall back to index.js.
Bare specifiers search node_modules:
Look in ./node_modules, then each parent directory's node_modules, up to the filesystem root.
The package's "exports"/"main" field then picks the entry file.
Result: The resolved absolute path becomes the cache key; if nothing matches, Node throws MODULE_NOT_FOUND.
Q57.What is the require cache in Node.js, and why are modules only executed once?
require cache in Node.js, and why are modules only executed once?The require cache (require.cache) is an object keyed by resolved absolute file path that stores each module's exports after its first load; subsequent require calls return the cached exports instead of re-running the file.
Executed once: A module's top-level code runs only on first require; later requires reuse the same module.exports object.
Why it matters:
Performance: avoids re-parsing and re-executing.
Singletons: shared state (config, DB pool) is the same instance everywhere it's imported.
Cache key is the resolved path: Different paths to the "same" package can produce separate instances.
You can manipulate it: Deleting require.cache[path] forces a reload (used in some hot-reload tooling, rarely in production).
Q58.Why is it considered a best practice to separate your 'app' definition from your 'server' listener (e.g., app.js vs. server.js)?
app.js vs. server.js)?Separating the app (Express instance plus routes and middleware) from the server (the process that binds a port and listens) makes the app importable and testable without ever opening a socket.
Testability: Tools like supertest import the app object directly and make requests in-memory, with no real port and no leaked listeners between tests.
Separation of concerns: app.js defines behavior (routes, middleware); server.js handles environment concerns (port, TLS, clustering, graceful shutdown).
Reusability: The same app can be mounted under different servers or serverless adapters (e.g. wrapped for AWS Lambda) without changing route code.
Single listen point: Only server.js calls app.listen(), avoiding accidental multiple binds when the module is imported in several places.
Q59.What is the difference between app.use() and app.all() in an Express application?
app.use() and app.all() in an Express application?app.use() mounts middleware that runs for any HTTP method and matches a path prefix, while app.all() registers a route handler that matches every method but only for an exact path pattern.
app.use() is for middleware:
Matches any method and treats the path as a prefix: app.use('/api', ...) also matches /api/users.
Often called with no path so it runs on every request (logging, body parsing).
app.all() is for routing:
Matches all methods but the full path must match the route pattern, like app.get() without the method restriction.
Useful for applying logic to one endpoint regardless of method (e.g. auth on /admin).
Key distinction: use = prefix matching for cross-cutting middleware; all = exact-path matching for a specific route.
Q60.Explain the difference between 'App-level middleware' and 'Router-level middleware' in Express.
Express.Both are the same kind of function; the difference is what they are bound to: app-level middleware is attached to the main app instance, while router-level middleware is attached to an isolated express.Router() that you mount as a sub-module.
App-level middleware: Registered with app.use() or app.METHOD() and applies globally across the whole application.
Router-level middleware:
Registered on a router instance and only runs for routes handled by that router.
Lets you group related routes and scope middleware (e.g. auth only on the admin router).
Mounting connects them: app.use('/users', userRouter) attaches a router under a path prefix, keeping route definitions modular.
Q61.How do you implement a global error-handling middleware in an Express application?
Express application?Define a middleware with four arguments (err, req, res, next); Express recognizes that signature as an error handler and routes any error passed to next(err) to it. Register it last, after all routes.
The four-arg signature is required: Express distinguishes error middleware purely by arity; omitting err makes it a normal middleware.
Trigger it with next(err): Synchronous throws in a route are caught automatically; in async handlers you must pass errors to next() (or use a wrapper) so they reach the handler.
Place it last: Register after all routes so unmatched errors fall through to it; you can centralize status codes and response shape here.
Respond consistently: Set a status, log the error, and return a clean message without leaking stack traces in production.
Q62.Explain the error-handling middleware signature in Express and how it differs from regular middleware.
Express and how it differs from regular middleware.Error-handling middleware is identified by its arity: it takes four arguments (err, req, res, next), and Express only routes errors to functions with that exact signature, whereas regular middleware takes (req, res, next).
The four-parameter signature is the marker:
Express inspects fn.length; if it's 4, the function is treated as error-handling.
You must declare all four params even if you don't use next, or it won't be recognized.
It runs only when an error is propagated:
Triggered by calling next(err) (or, in 5.x, an unhandled rejection), not on normal requests.
Normal middleware in the chain is skipped once an error is in flight; Express jumps to the next error handler.
Placement and chaining:
Define it last, after all routes, so it catches errors from everything before it.
Call next(err) inside it to delegate to another error handler, or send a response to end the cycle.
Q63.What causes the 'Cannot set headers after they are sent to the client' error, and how do you prevent it conceptually?
Cannot set headers after they are sent to the client' error, and how do you prevent it conceptually?This error occurs when you try to modify or send the HTTP response after it has already been sent: the headers were flushed to the client, so any further write (status, header, or a second res.send) is illegal. Conceptually you prevent it by ensuring exactly one response per request and stopping execution after sending.
The response lifecycle is one-way: Once headers are written, res.headersSent becomes true and they can't be changed.
Common causes:
Calling res.send/res.json twice on the same path.
Forgetting to return after sending, so code keeps running and sends again.
Sending inside a callback and also after it, or in both branches of async flow.
How to prevent it conceptually:
Guarantee a single exit point: return res.send(...) to stop further execution.
Use control flow (if/else, early returns) so only one branch responds.
Let errors flow to next(err) instead of responding ad hoc in multiple places.
Q64.What happens to a Node.js process when an 'unhandledRejection' occurs in modern Node versions?
unhandledRejection' occurs in modern Node versions?In modern Node.js (since v15), an unhandled promise rejection terminates the process by default, just like an uncaught exception, instead of only printing a deprecation warning as older versions did.
Default mode: throw:
Node emits the unhandledRejection event; if nothing handles it, the process crashes with a non-zero exit code.
This treats a forgotten .catch() or un-awaited rejection as a real bug.
You can listen for it: process.on('unhandledRejection', handler) lets you log/clean up, but the recommended response is still to exit gracefully.
The mode is configurable: --unhandled-rejections=warn (warn only), strict, or none change the behavior, but the safe default is to crash.
Best practice: handle rejections at the source: Always await inside try/catch or attach .catch(); treat a crash as a signal of a missing handler.
Q65.How does the EventEmitter work internally, and what happens if you don't remove listeners?
EventEmitter work internally, and what happens if you don't remove listeners?Internally an EventEmitter keeps a map of event names to arrays of listener functions; emit looks up that array and calls each function in order. If you keep adding listeners without removing them, those arrays (and anything they close over) are retained, causing memory leaks.
Internal structure:
A _events object maps each event name to a single listener or an array of them.
on pushes onto that array; emit iterates and invokes synchronously with the provided args.
once wraps your listener so it removes itself after firing once.
Leak risk if listeners aren't removed:
Long-lived emitters that gain a listener per request/connection grow unbounded, retaining closures and their captured objects.
Node warns at 10 listeners per event by default (MaxListenersExceededWarning) as a leak hint.
How to manage it:
Use off/removeListener when done, prefer once for one-shot events.
Adjust limits deliberately with setMaxListeners only when many listeners are legitimately expected.
Q66.What are the advantages of using the built-in Node.js Test Runner (introduced in Node 20+) over external libraries like Jest?
Node.js Test Runner (introduced in Node 20+) over external libraries like Jest?The built-in node:test runner offers zero-dependency, zero-configuration testing baked into the runtime, avoiding the install footprint, version churn, and transpilation setup that external frameworks like Jest often require.
No external dependencies: Ships with Node, so there's nothing to install, fewer node_modules to audit, and no risk of supply-chain or version-mismatch issues.
Minimal configuration: Works out of the box; no config files or transform setup needed for plain JS or ESM.
Native and fast: Integrates directly with the runtime, with built-in TAP/reporter output, node --test, watch mode, coverage, and built-in mocking via mock.
Stays current with the runtime: Tracks new Node features (ESM, etc.) without waiting on a third-party library to catch up.
Trade-off to acknowledge: Jest still has a richer ecosystem (snapshots, extensive matchers, large plugin community), so the built-in runner is leaner but less feature-complete.
Q67.Explain the difference between spawn(), exec(), and fork() in the child_process module.
spawn(), exec(), and fork() in the child_process module.All three create child processes, but they differ in how they run the command and handle output: spawn() streams output for long-running processes, exec() buffers output for short commands, and fork() is a specialized spawn for new Node processes with a built-in IPC channel.
spawn():
Launches a command and returns streams (stdout/stderr); ideal for large or continuous output since data flows incrementally and isn't buffered in memory.
Does not spawn a shell by default.
exec():
Runs a command in a shell and buffers the entire output, passing it to a callback when done; convenient for short commands but risks maxBuffer overflow on large output.
Because it uses a shell, beware command injection with untrusted input.
fork():
A special case of spawn() that runs a new Node.js module as a child process and sets up an IPC channel for send()/'message' messaging between parent and child.
Each fork is a full separate process with its own V8 instance and memory (heavier than worker threads).
Q68.What is the difference between the cluster module and worker_threads?
cluster module and worker_threads?The cluster module scales across cores by forking separate Node processes (each with its own memory), while worker_threads runs multiple threads inside a single process that can share memory: cluster is for scaling I/O-bound servers, threads are for CPU-bound work.
Isolation model:
cluster: separate OS processes, no shared memory, communicate via IPC message passing.
worker_threads: threads in one process that can share memory via SharedArrayBuffer and pass messages cheaply.
Best use case:
cluster: scaling a network/HTTP server across cores, since workers share a listening port.
worker_threads: offloading CPU-intensive computation (parsing, image processing, crypto) without blocking the main event loop.
Overhead: Processes (cluster) are heavier (full memory copy) than threads, which share the process's resources.
Rule of thumb: Scale I/O concurrency with cluster; speed up heavy computation with worker_threads.
Q69.Explain the difference between child_process.spawn() and child_process.fork().
child_process.spawn() and child_process.fork().Both create child processes, but spawn() launches any external command/binary while fork() is a specialized form that spawns a new Node.js process with a built-in IPC channel for message passing.
spawn() runs arbitrary commands:
Executes any executable (e.g. ls, python, ffmpeg) and streams stdout/stderr back, ideal for large output.
No IPC channel by default; you communicate through standard streams.
fork() runs Node modules:
A special case of spawn() that always runs a new V8/Node instance of a given JS file.
Automatically sets up an IPC channel so parent and child exchange messages via child.send() and process.on('message').
Use case rule of thumb: Reach for spawn() to call external programs; reach for fork() to run additional Node workers that talk back.
Q70.When should you use worker threads instead of simply spawning a new process?
worker threads instead of simply spawning a new process?Use worker threads for CPU-bound JavaScript work where you want low communication overhead and the ability to share memory; spawn a separate process for isolation, running external programs, or fault tolerance where a crash shouldn't take down siblings.
Prefer worker threads when:
You have CPU-intensive JS (parsing, hashing, image/number crunching) that would block the event loop.
You want cheaper startup and shared memory via SharedArrayBuffer instead of copying data.
Threads live in one process, so they share the V8 instance more efficiently than full processes.
Prefer a separate process when:
You need strong isolation: a crash or memory bloat in one shouldn't affect others.
You're running non-Node code or external binaries (that's spawn()).
You're scaling across CPU cores for concurrent I/O work (the cluster model).
Key trade-off: Threads = lighter and shared memory but less isolation; processes = heavier and isolated but copy data over IPC.
Q71.How do worker threads communicate with the main thread?
Worker threads communicate primarily through message passing over a MessagePort: the main thread and worker each call postMessage() and listen for 'message' events, with optional shared memory for high-performance cases.
postMessage() / 'message':
Parent uses worker.postMessage() and worker.on('message'); inside the worker you use parentPort.postMessage() and parentPort.on('message').
Data is copied using the structured clone algorithm.
workerData: Initial data passed once at creation, available in the worker as workerData.
Transfer and shared memory:
You can transfer an ArrayBuffer (zero-copy, ownership moves) by passing it in the transfer list.
A SharedArrayBuffer is genuinely shared (no copy), so both threads see the same memory.
MessageChannel / MessagePort let workers also talk directly to each other.
Q72.How do you debug and profile a Node.js application using the --inspect flag and Chrome DevTools?
--inspect flag and Chrome DevTools?The --inspect flag starts the V8 inspector and opens a WebSocket debugging port, letting you attach Chrome DevTools (or VS Code) to set breakpoints, step through code, inspect variables, and capture CPU/heap profiles of a live process.
Starting the inspector:
Run node --inspect app.js to listen on the default port 9229; use --inspect-brk to break before the first line so you can debug startup code.
Attach by opening chrome://inspect in Chrome and clicking the target, or attach VS Code's debugger.
Debugging features: Breakpoints, step in/over/out, watch expressions, the call stack, and a live console scoped to the paused frame.
Profiling:
The Profiler tab records a CPU profile (flame chart) to find hot functions; the Memory tab takes heap snapshots to find leaks and retained objects.
Compare two heap snapshots over time to spot objects that keep growing.
Caveats:
Never expose the inspector port in production: anyone who connects gets full code execution. Bind to localhost and use SSH tunneling if remote.
For headless profiling use node --prof or the clinic toolset.
Q73.What does the crypto module provide, and how would you use it to securely hash a password?
crypto module provide, and how would you use it to securely hash a password?The built-in crypto module provides cryptographic primitives: hashing, HMAC, symmetric/asymmetric encryption, signing, and secure random bytes. For passwords you must use a slow, salted key-derivation function like scrypt (or bcrypt/argon2 from libraries), never a fast hash like SHA-256.
What crypto offers: createHash / createHmac, createCipheriv, key derivation (scrypt, pbkdf2), and CSPRNG via randomBytes.
Why a plain hash is wrong for passwords: SHA/MD5 are fast, so attackers brute-force billions per second; KDFs are deliberately slow and memory-hard.
Do it correctly:
Generate a unique random salt per password and store it alongside the hash.
Verify with a constant-time comparison (crypto.timingSafeEqual) to avoid timing attacks.
Q74.What is the role of libuv in Node.js, and how does it interact with the V8 engine?
libuv in Node.js, and how does it interact with the V8 engine?libuv is the C library that gives Node its event loop and asynchronous I/O: it abstracts platform differences and manages the thread pool, while V8 executes the JavaScript that schedules and consumes that work.
Provides the event loop: Drives the phases (timers, pending callbacks, poll, check, close) that decide which callbacks run next.
Cross-platform async I/O: Wraps epoll/kqueue/IOCP for sockets and a thread pool for file, DNS, and crypto work.
How it meets V8:
V8 runs the JS; when JS calls something like fs.readFile, Node's C++ bindings hand the request to libuv.
When libuv signals completion, the binding invokes the JS callback back inside V8.
Division of labor: V8 = execute/compile JavaScript; libuv = scheduling and I/O. Together they form Node's async runtime.
Q75.Explain the role of the V8 engine within Node.js and how it interacts with the C++ bindings.
V8 engine within Node.js and how it interacts with the C++ bindings.V8 is Google's JavaScript engine that compiles and runs your JS code; Node embeds it and connects it to system capabilities through C++ bindings, so JavaScript can trigger native operations it could never do on its own.
V8 executes JavaScript:
JIT-compiles JS to machine code and manages memory (the heap and garbage collection).
Knows nothing about files, sockets, or timers by itself.
C++ bindings bridge the gap: Node exposes native functions to JS; calling a built-in like fs.read crosses into C++ that talks to the OS or libuv.
Two-way flow: JS values are converted to C++ types on the way in, and results are converted back to JS values (and callbacks invoked) on the way out.
Why it matters: This embedding is what turns a browser-style engine into a server runtime with full system access.
Q76.Explain the Reactor Pattern in the context of Node.js.
Node.js.The Reactor Pattern is the design behind Node's event loop: instead of blocking on I/O, you register handlers for events and a central dispatcher (the reactor) reacts by invoking the right callback when each operation completes.
Core idea: Each I/O request is submitted with a handler, then control returns immediately; nothing waits.
The demultiplexer: The OS/libuv watches many resources at once and reports which are ready (the event demultiplexer).
The event queue and loop: Ready events with their callbacks are queued; the event loop pulls them and runs each handler one at a time.
Why Node uses it: It lets a single thread handle thousands of concurrent connections cheaply, with no thread-per-request overhead.
Q77.What is the difference between hard and soft limits in the context of Node.js process resources?
Hard and soft limits are OS-level resource ceilings (file descriptors, memory, processes) that apply to the Node process. The soft limit is the currently enforced value; the hard limit is the maximum the soft limit may be raised to without elevated privileges.
Soft limit:
The value actively enforced; hitting it triggers errors like `EMFILE` (too many open files).
A process can raise its own soft limit up to the hard limit.
Hard limit: The ceiling for the soft limit; raising it usually requires root/admin privileges.
Relevance to Node:
A server with many sockets/files can exhaust the FD soft limit; check and tune with `ulimit -n` (Linux/macOS).
Node's V8 heap has its own separate cap, adjustable via `--max-old-space-size`.
Q78.How do you implement a graceful shutdown for a Node.js server to ensure no active requests are dropped, handling SIGTERM and SIGINT?
SIGTERM and SIGINT?Q79.Why is process.nextTick() considered potentially 'dangerous' if used recursively?
process.nextTick() considered potentially 'dangerous' if used recursively?Q80.How does the 'Poll' phase differ from the 'Check' phase in the event loop?
Q81.What is 'event loop starvation,' and what kind of code causes it?
Q82.How does Node.js handle DNS resolution, and why can it sometimes block the thread pool?
Node.js handle DNS resolution, and why can it sometimes block the thread pool?Q83.What are the common causes of Event Loop Lag, and how do you monitor it?
Q84.What is 'Backpressure' in Node.js streams, and how do you conceptually handle it?
Node.js streams, and how do you conceptually handle it?Q85.What is 'object mode' in a Node.js stream, and when would you enable it?
Q86.Explain the concept of 'live bindings' in ES Modules vs. 'value copies' in CommonJS.
Q87.How does Node.js handle circular dependencies in CommonJS?
CommonJS?Q88.How do you handle unhandledRejection and uncaughtException in a production Node environment?
unhandledRejection and uncaughtException in a production Node environment?Q89.How does Express handle asynchronous errors in version 5.x compared to version 4.x?
Express handle asynchronous errors in version 5.x compared to version 4.x?Q90.What is AsyncLocalStorage, and what problem does it solve for tracking context across asynchronous calls?
AsyncLocalStorage, and what problem does it solve for tracking context across asynchronous calls?Q91.How does the cluster module distribute incoming connections across multiple CPU cores?
cluster module distribute incoming connections across multiple CPU cores?Q92.What are the differences between worker_threads, cluster, and child_process?
worker_threads, cluster, and child_process?Q93.What are the trade-offs of using Worker Threads for CPU-intensive tasks versus offloading them to a separate microservice?
Worker Threads for CPU-intensive tasks versus offloading them to a separate microservice?Q94.What is Inter-Process Communication (IPC) in Node.js, and how is it implemented between a master and worker process?
Node.js, and how is it implemented between a master and worker process?Q95.What are 'zombie processes' in Node.js, and how can they be prevented when using child processes?
Node.js, and how can they be prevented when using child processes?Q96.How does 'Zero-downtime deployment' work conceptually with the Node.js cluster module?
Node.js cluster module?Q97.How do worker_threads share memory, and what is the role of SharedArrayBuffer?
worker_threads share memory, and what is the role of SharedArrayBuffer?Q98.How do you identify and diagnose a memory leak in a Node.js application?
Node.js application?Q99.How does the V8 garbage collector work in Node.js (Scavenge vs. Mark-Sweep)?
V8 garbage collector work in Node.js (Scavenge vs. Mark-Sweep)?Q100.How does Node.js handle garbage collection, and how can you detect a memory leak in a running process?
Q101.What is the ReDoS (Regular Expression Denial of Service) attack, and why is it dangerous for Node.js?
Q102.What is 'Prototype Pollution' in the context of Node.js, and how can it be mitigated?
Q103.Explain the security implications of using eval() or vm.runInContext() in a Node.js server.
eval() or vm.runInContext() in a Node.js server.