🟢 Expert Node.js Interview Questions
This lesson targets senior engineers and architects. Topics include V8 internals, libuv, memory management, garbage collection, the event loop in depth (microtasks, phases, starvation), N-API native addons, security, module resolution internals, Corepack, the Permission Model, and production architecture patterns. These questions reveal whether you understand Node.js or merely write JavaScript in it.
Questions & Answers
01 What is V8 and how does it execute JavaScript? ►
V8 Internals V8 is Google’s open-source JavaScript and WebAssembly engine, written in C++. Node.js embeds V8 to execute JavaScript code.
V8 compilation pipeline:
- Parsing โ JavaScript source is parsed into an Abstract Syntax Tree (AST). V8 uses lazy parsing: functions are only fully parsed when called (except exported functions and IIFEs).
- Ignition (bytecode interpreter) โ the AST is compiled to bytecode and executed by the Ignition interpreter. Fast to start, not the fastest to run.
- TurboFan (optimising JIT compiler) โ functions called frequently (hot functions) are identified and compiled by TurboFan to highly optimised native machine code. This is just-in-time (JIT) compilation.
- Deoptimisation โ if V8’s type assumptions are violated (e.g., an array that was always ints suddenly gets a string), TurboFan deoptimises back to Ignition. This is why monomorphic code (consistent types) is faster.
// Help V8 optimise โ consistent types
function add(a, b) { return a + b; }
add(1, 2); // V8 assumes Number + Number
add(3, 4); // confirms โ TurboFan optimises
add('x', 2); // type change โ deoptimisation triggered
// Use --v8-options to list V8 flags
// node --v8-options | grep 'trace-opt'
// node --trace-opt server.js โ shows which functions TurboFan optimises
02 What is libuv and what role does it play in Node.js? ►
Internals libuv is a cross-platform C library that powers Node.js’s asynchronous I/O operations. V8 runs JavaScript but cannot interact with the OS; libuv provides the event loop, thread pool, and system call abstractions.
libuv components:
- Event loop โ the core loop that polls for I/O readiness using OS-specific mechanisms:
epoll(Linux),kqueue(macOS/BSD),IOCP(Windows) - Thread pool โ a pool of 4 threads (configurable via
UV_THREADPOOL_SIZE) for operations the OS cannot do asynchronously: file I/O, DNS resolution (dns.lookup), some crypto operations,fs.stat - Handles and requests โ handles represent long-lived objects (TCP connections, timers); requests represent short-lived operations (a write to a socket)
- Timer implementation โ
setTimeout/setIntervalare implemented in libuv using a binary heap of timer callbacks
# Increase thread pool size for file-heavy workloads UV_THREADPOOL_SIZE=16 node server.js # Why this matters: if you have 4 threads and 10 concurrent fs.readFile() # calls, 6 will queue and wait. Increasing the pool helps I/O-heavy workloads.
03 Explain Node.js memory management and garbage collection. ►
Memory Node.js memory is managed by V8’s garbage collector. Understanding it helps diagnose memory leaks and tune performance.
Heap structure:
- New Space (Young Generation) โ small (~1-8MB), frequently collected (Scavenge GC). Short-lived objects live here. Most objects die young.
- Old Space (Old Generation) โ objects that survived 2+ Scavenge cycles are promoted here. Collected by Major GC (Mark-Sweep-Compact). Larger, less frequent.
- Large Object Space โ objects exceeding the size threshold (typically 256KB) that would not fit in a normal page.
- Code Space โ JIT-compiled code from TurboFan.
# Default heap limit (~1.5GB on 64-bit)
node --max-old-space-size=4096 server.js # set to 4GB
# Monitor GC activity
node --expose-gc --trace-gc server.js
# Force GC (development/testing only โ NEVER in production)
if (global.gc) global.gc();
// Check heap usage
const v8 = require('v8');
const stats = v8.getHeapStatistics();
console.log({
used: Math.round(stats.used_heap_size / 1024 / 1024) + 'MB',
total: Math.round(stats.total_heap_size / 1024 / 1024) + 'MB',
limit: Math.round(stats.heap_size_limit / 1024 / 1024) + 'MB'
});
Common memory leaks: closures holding references to large objects, event listeners not removed (EventEmitter.off()), global variables, timers not cleared, circular references in Buffers, keeping references in Maps/Sets without clearing them.
04 What is the event loop microtask queue starvation problem? ►
Event Loop Microtask starvation occurs when the microtask queue (Promises, queueMicrotask, process.nextTick) is continuously refilled, preventing the event loop from ever advancing to its I/O and timer phases. The server stops processing new requests.
// โ DANGER: recursive Promise chain โ starves event loop
async function recursiveAsync() {
await Promise.resolve(); // queues to microtask queue
recursiveAsync(); // immediately queues another
// event loop NEVER proceeds to I/O phase โ all HTTP requests hang
}
// โ Same with process.nextTick
function starveTick() {
process.nextTick(starveTick); // fills nextTick queue infinitely
}
// โ
FIX: use setImmediate to yield to I/O between iterations
async function safeBatch(items) {
for (const item of items) {
await processItem(item);
// Yield to event loop every 100 items
if (items.indexOf(item) % 100 === 0) {
await new Promise(resolve => setImmediate(resolve));
}
}
}
// โ
FIX for CPU work: use setImmediate to release the event loop
function processLargeArray(arr, callback) {
let i = 0;
function doChunk() {
let n = 1000; // process 1000 items per tick
while (n-- && i < arr.length) process(arr[i++]);
if (i < arr.length) setImmediate(doChunk); // yield, then continue
else callback();
}
setImmediate(doChunk);
}
05 What is N-API and why is it important for native addons? ►
Native N-API (now called Node-API) is a stable C API for building native Node.js addons โ modules written in C/C++ (or Rust via napi-rs) that are callable from JavaScript.
Why Node-API matters:
- ABI stability โ N-API is ABI-stable across Node.js versions. Before N-API, native addons broke on every Node.js version upgrade because they depended on V8’s internal API (which changes). N-API addons compiled for Node 14 work on Node 20.
- Language bridging โ call C++ libraries (OpenCV, TensorFlow, libsodium) or Rust crates from JavaScript
- Performance-critical code โ offload CPU-intensive work to native code
// Native addon written with node-addon-api (C++ wrapper for N-API)
// addon.cc
#include <napi.h>
Napi::Value Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
double arg0 = info[0].As<Napi::Number>().DoubleValue();
double arg1 = info[1].As<Napi::Number>().DoubleValue();
return Napi::Number::New(env, arg0 + arg1);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("add", Napi::Function::New(env, Add));
return exports;
}
NODE_API_MODULE(addon, Init)
// Usage from JavaScript
const addon = require('./build/Release/addon');
console.log(addon.add(3, 4)); // 7
// Modern approach: napi-rs (Rust โ Node.js) or ffi-napi (call .so/.dll directly)
06 What is the Node.js Permission Model? ►
Security The Permission Model (experimental in Node.js 20, maturing in Node 22+) restricts what resources a Node.js process can access at runtime โ file system paths, network, child processes, workers, and native addons. Similar to Deno’s permission system.
# Run with specific permissions only
node --experimental-permission server.js
# By default ALL access is blocked
# Grant read access to specific directory
node --experimental-permission \
--allow-fs-read=/app/public \
server.js
# Grant write access
node --experimental-permission \
--allow-fs-write=/app/logs \
--allow-fs-read=/app \
server.js
# Allow network access (optional โ can restrict to specific hosts)
node --experimental-permission \
--allow-net \
server.js
# Allow child process spawning
node --experimental-permission \
--allow-child-process \
server.js
# Allow worker threads
node --experimental-permission \
--allow-worker \
server.js
// Check permissions programmatically
const { permission } = require('node:process');
console.log(permission.has('fs.read', '/app/data')); // true/false
// Attempt blocked operation โ throws an error
try { fs.readFileSync('/etc/passwd'); }
catch (e) { console.error(e.code); } // ERR_ACCESS_DENIED
The Permission Model is valuable for running untrusted scripts, multi-tenant systems, and hardening production containers by applying the principle of least privilege.
07 What is Corepack and how does it manage package manager versions? ►
Tooling Corepack is a Node.js built-in tool (included since Node.js 16.9+) that manages the version of npm, Yarn, or pnpm used in a project โ ensuring everyone on the team uses the exact same package manager version without global installs.
# Enable Corepack (disabled by default)
corepack enable
# package.json โ specify package manager and version
{
"packageManager": "pnpm@9.1.0"
}
# Now pnpm is automatically downloaded and used at the specified version
pnpm install # uses pnpm 9.1.0 exactly (downloaded if not cached)
# Prepare a specific version (pre-download for offline use)
corepack prepare pnpm@9.1.0 --activate
# Switch between package managers per project
echo '{"packageManager": "yarn@4.2.2"}' > package.json
yarn install # Corepack downloads Yarn 4.2.2
# Why this matters:
# Without Corepack, developers each install different npm/yarn/pnpm versions
# This causes lockfile format differences, inconsistent behaviour, and CI/CD issues
# Corepack pins the package manager version just like node engines pins Node
08 How does Node.js module resolution work? What is the resolution algorithm? ►
Modules When you call require('some-module'), Node.js follows a specific resolution algorithm to find the file:
// require('./utils') โ relative path resolution
// 1. Try exact: ./utils.js โ ./utils.json โ ./utils.node
// 2. Try directory: ./utils/index.js โ ./utils/index.json โ ./utils/index.node
// 3. Check package.json "main" field in ./utils/package.json
// require('express') โ bare specifier (npm package)
// Starting from current directory, walk UP the tree:
// ./node_modules/express โ ../node_modules/express โ ../../node_modules/express
// Until reaching the filesystem root or finding the module
// Inside node_modules/express/:
// 1. Check package.json "main" field โ e.g., "./index.js"
// 2. Fall back to index.js, index.json, index.node
// require('/absolute/path/to/file') โ absolute path
// Tries .js, .json, .node extensions if no extension given
// package.json "exports" field (modern โ overrides "main")
// Enables conditional exports: different files for CJS vs ESM, browser vs Node
{
"exports": {
".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" },
"./utils": "./dist/utils.js",
"./package.json": "./package.json"
}
}
// require('mylib') โ dist/index.cjs
// import 'mylib' โ dist/index.mjs
// import 'mylib/utils' โ dist/utils.js
// import 'mylib/internal' โ โ not exported โ blocked
09 How do you detect and fix memory leaks in a production Node.js app? ►
Memory
Step 1 โ Detect: watch memory usage over time
// Add memory monitoring to your app
setInterval(() => {
const mem = process.memoryUsage();
logger.info({
rss: Math.round(mem.rss / 1024 / 1024) + 'MB', // total process memory
heapUsed: Math.round(mem.heapUsed / 1024 / 1024) + 'MB', // JS heap in use
heapTotal:Math.round(mem.heapTotal/ 1024 / 1024) + 'MB', // total JS heap
external: Math.round(mem.external / 1024 / 1024) + 'MB' // C++ objects (Buffers)
}, 'Memory usage');
}, 30000); // log every 30s โ a rising heapUsed trend indicates a leak
Step 2 โ Isolate: heap snapshots in Chrome DevTools
node --inspect server.js # Open chrome://inspect โ Memory tab โ Take heap snapshot # Apply load with autocannon, take another snapshot # "Comparison" view shows which object types grew # Look for: detached DOM nodes, growing arrays, uncleaned event listeners
Step 3 โ Common causes and fixes:
- Event listeners not removed โ
emitter.off()in cleanup - Timer not cleared โ
clearInterval()/clearTimeout() - Growing global Map/Set โ add TTL or max-size eviction
- Closures capturing large objects โ
nullthe reference when done - Unclosed streams โ always handle
end/close/errorevents - Promise chains not completing โ add
.catch()and timeouts
// Tool: clinic.js โ automated profiling suite npm install -g clinic clinic doctor -- node server.js # detects I/O, event loop, memory issues clinic heap -- node server.js # heap profiler clinic flame -- node server.js # CPU flame graph
10 What are the OWASP Node.js security best practices? ►
Security
- Prototype pollution prevention โ never merge untrusted objects into existing objects. Use
Object.create(null)for safe maps without__proto__. Validate withno-prototype-builtinsESLint rule. - Dependency security โ run
npm auditin CI/CD. Automate with Snyk or Dependabot. Prefer packages with a small, well-maintained dependency tree. - ReDoS (RegEx DoS) โ avoid catastrophically backtracking regular expressions that can hang the event loop on malicious input. Use the
safe-regexpackage to detect them. - Timing attacks โ use
crypto.timingSafeEqual()for comparing secrets/hashes. Standard string comparison (===) leaks timing information. - Path traversal โ sanitise file path inputs. Use
path.resolve()and verify the result starts with your intended directory. - SSRF (Server-Side Request Forgery) โ validate URLs before making outbound requests. Block private IP ranges (169.254.x.x, 10.x.x.x).
- Environment variables โ never log
process.env. Use secrets managers (AWS Secrets Manager, HashiCorp Vault) in production. - Least privilege โ run Node.js as a non-root user. Use the Permission Model. Scope IAM roles to minimum required permissions.
// Path traversal prevention
function safeReadFile(userPath) {
const safeBase = path.resolve('/app/uploads');
const requested = path.resolve(safeBase, userPath);
if (!requested.startsWith(safeBase)) {
throw new Error('Path traversal detected');
}
return fs.readFileSync(requested);
}
11 What are async_hooks and how are they used for request tracing? ►
Async Context async_hooks provide lifecycle hooks for every async resource created in Node.js โ init, before, after, destroy, promiseResolve. They are the internal mechanism behind AsyncLocalStorage and are used by APM tools for distributed tracing.
const async_hooks = require('async_hooks');
const fs = require('fs');
// Track async resource lifetimes (low-level, rarely used directly)
const hook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
fs.writeSync(1, `init: asyncId=${asyncId} type=${type} trigger=${triggerAsyncId}\n`);
// type: PROMISE, TCPWRAP, TIMERWRAP, HTTPPARSER, etc.
},
before(asyncId) {
fs.writeSync(1, `before: asyncId=${asyncId}\n`);
},
after(asyncId) {
fs.writeSync(1, `after: asyncId=${asyncId}\n`);
},
destroy(asyncId) {
fs.writeSync(1, `destroy: asyncId=${asyncId}\n`);
}
});
hook.enable();
// Note: console.log is NOT safe inside async_hooks callbacks (it's async!)
// Use fs.writeSync (synchronous) for debugging inside hooks
// WHY ASYNC_HOOKS MATTERS:
// AsyncLocalStorage (lesson 2) is built on async_hooks
// OpenTelemetry uses async_hooks to propagate trace context
// APM tools (Datadog, New Relic) use it to track all async operations
// Most developers use AsyncLocalStorage instead of raw async_hooks
const { AsyncLocalStorage } = require('async_hooks');
// (Prefer this โ safe, stable, simpler API)
12 What is the Node.js startup performance? How do you optimise it? ►
Performance Cold start time matters in serverless functions (AWS Lambda, Vercel, Cloud Run) where every invocation may start a fresh Node.js process.
Optimisation techniques:
- Lazy require() โ only load modules when first needed, not at startup
- V8 code cache โ Node.js can serialize V8’s compiled bytecode to disk (
--code-cache-pathflag), skipping recompilation on restart - Node.js Single Executable Application (SEA) โ Node 20+ can bundle an app into a single self-contained executable, eliminating module resolution overhead
- Reduce dependencies โ each
require()costs time (file I/O + parsing). Use bundle tools likeesbuildornccto create a single-file bundle
# Bundle the entire app to a single file (no node_modules at runtime)
npx @vercel/ncc build src/server.js -o dist/
# Result: dist/index.js โ single file with all dependencies inlined
# Measure startup time
time node -e "require('./src/server')"
# Profile startup with --cpu-prof
node --cpu-prof --cpu-prof-interval=100 server.js
# Generates a .cpuprofile file โ open in Chrome DevTools Performance tab
# V8 startup snapshot (experimental Node 20+)
# Serialise initialised heap state to a snapshot file
# Future processes restore from snapshot โ skipping init work
13 What is the Node.js SEA (Single Executable Application)? ►
Packaging Node.js 20+ supports bundling an application into a standalone executable that includes Node.js itself โ no Node.js installation required on the target machine. Similar to Deno’s deno compile.
// 1. Create SEA configuration
// sea-config.json
{
"main": "dist/app.js", // entry point (must be a single bundled file)
"output": "sea-prep.blob", // intermediate blob
"disableExperimentalSEAWarning": true,
"useSnapshot": false,
"useCodeCache": true // cache V8 bytecode for faster startup
}
// 2. Bundle your app first (SEA requires a single file)
npx esbuild src/server.js --bundle --platform=node --outfile=dist/app.js
// 3. Generate the blob
node --experimental-sea-config sea-config.json
// 4. Copy the Node.js binary and inject the blob
cp $(which node) my-app
# On macOS โ remove existing signature:
codesign --remove-signature my-app
# Inject the blob:
npx postject my-app NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
--macho-segment-name NODE_SEA
// 5. Distribute and run โ no Node.js needed on target machine
./my-app
SEA is ideal for CLI tools, desktop apps, and container images where you want a single binary with no runtime dependency.
14 What is the difference between EventEmitter’s sync and async event handling? ►
Events EventEmitter is synchronous by design โ emit() calls all listeners synchronously in the order they were registered, returning only after all listeners complete. This surprises many developers who expect asynchronous behaviour.
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
emitter.on('data', (val) => console.log('listener 1:', val));
emitter.on('data', (val) => console.log('listener 2:', val));
console.log('before emit');
emitter.emit('data', 'hello'); // SYNCHRONOUS โ all listeners run before returning
console.log('after emit');
// Output:
// before emit
// listener 1: hello
// listener 2: hello
// after emit โ NOT before the listeners!
// โ DANGER: async listener throws โ unhandled error
emitter.on('data', async (val) => {
await doSomething();
throw new Error('oops'); // this is an UNHANDLED rejection โ not caught by emit()
});
// โ
Handle async errors in async listeners
emitter.on('data', async (val) => {
try {
await doSomething();
} catch (err) {
emitter.emit('error', err); // propagate to error listener
}
});
// EventEmitterAsyncResource (Node 17+) โ integrates async context tracking
// with EventEmitter so that async listeners preserve AsyncLocalStorage context
15 What are the Node.js LTS release schedule and version support policy? ►
Ecosystem
- Major versions โ released every 6 months. Even-numbered versions (18, 20, 22) become LTS (Long-Term Support); odd-numbered versions (17, 19, 21) are short-lived Current releases.
- LTS lifecycle โ Active LTS: 18 months after release (bug fixes, security patches). Maintenance LTS: 12 more months (security patches only). Total: ~3 years of support per LTS version.
- Current โ latest features, 6 months of support. Use only for experimentation.
# Check current version node --version # Node.js 20 LTS (Codename: Iron) โ Active LTS until April 2025, Maintenance until April 2026 # Node.js 22 LTS (Codename: Jod) โ Active LTS until April 2026, Maintenance until April 2027 # Node.js 24 โ Current release (2025-2026) # Use nvm (Node Version Manager) to manage multiple versions nvm install 22 # install latest Node 22 nvm use 22 # switch to Node 22 nvm alias default 22 # set as default nvm ls # list installed versions # .nvmrc file โ pin project to specific version echo "22" > .nvmrc nvm use # reads .nvmrc automatically
For production: always use the current Active LTS version. Upgrade to the next LTS within 3-6 months of its release to stay ahead of end-of-life. Use CI to test against multiple Node versions (node-version: [20, 22] in GitHub Actions).
16 What is the Node.js policy feature for supply chain security? ►
Security The Node.js Policy feature (experimental) allows you to define an integrity check policy for loaded modules โ ensuring every require()d file matches a pre-computed hash. This prevents supply chain attacks where a dependency is compromised after installation.
// policy.json โ define allowed modules and their integrity hashes
{
"resources": {
"./server.js": {
"integrity": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
},
"node_modules/express/index.js": {
"integrity": "sha384-EXPECTED_HASH_HERE"
}
}
}
# Run with the policy
node --experimental-policy=policy.json server.js
# If any file doesn't match its hash โ ERR_MANIFEST_ASSERT_INTEGRITY error
# Generate hashes for existing files
node -e "
const crypto = require('crypto');
const content = require('fs').readFileSync('./server.js');
const hash = crypto.createHash('sha384').update(content).digest('base64');
console.log('sha384-' + hash);
"
While still experimental, the Policy feature is one defence against attacks like event-stream (2018) where a popular npm package was compromised after transfer to a malicious maintainer. In practice, combine with npm audit, Sigstore/npm provenance, and lockfile integrity checking.
17 How does Node.js handle uncaughtException vs unhandledRejection? ►
Error Handling
// uncaughtException โ synchronous throw that reached the top of the call stack
process.on('uncaughtException', (err, origin) => {
// err = the Error object
// origin = 'uncaughtException' or 'unhandledRejection'
logger.fatal({ err, origin }, 'Uncaught exception โ process will exit');
// Clean up synchronously (close DB connections, flush logs)
// Then EXIT โ the process is now in an undefined state and must restart
process.exit(1);
});
// unhandledRejection โ Promise rejected without a .catch() or try/catch
process.on('unhandledRejection', (reason, promise) => {
// In Node.js <15: printed a warning
// In Node.js 15+: CRASHES the process by default (converted to uncaughtException)
logger.error({ reason, promise }, 'Unhandled promise rejection');
// Best practice: fix the root cause (add .catch()) rather than handling here
});
// rejectionHandled โ fired when a previously-unhandled rejection gets a handler attached
process.on('rejectionHandled', (promise) => {
logger.warn('Promise rejection handled late:', promise);
});
// Best practice: these are LAST RESORT handlers, not error handling strategies
// Fix unhandledRejection at source:
async function doWork() {
try {
await riskyOperation();
} catch (err) {
// handle here โ don't let it propagate to unhandledRejection
logger.error(err);
}
}
18 What is the Node.js import.meta object in ES Modules? ►
ESM In ES Modules, the CommonJS globals __dirname, __filename, require, and module are NOT available. import.meta provides module-level metadata as the ESM equivalent.
// ESM module (file.mjs or type: "module" in package.json)
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { createRequire } from 'module';
// __filename equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
// import.meta.url = "file:///home/user/app/server.mjs"
// __filename = "/home/user/app/server.mjs"
// __dirname equivalent in ESM
const __dirname = dirname(__filename);
// __dirname = "/home/user/app"
// require() equivalent in ESM (to load CJS modules)
const require = createRequire(import.meta.url);
const config = require('./config.json'); // load JSON the CJS way
// Other import.meta properties (bundler-specific)
console.log(import.meta.url); // file URL of this module
// import.meta.env โ Vite-specific (environment variables)
// import.meta.hot โ Vite HMR API
// import.meta.dirname โ Available in Node.js 21.2+ (no fileURLToPath needed!)
// import.meta.filename โ Available in Node.js 21.2+
19 How do you architect a Node.js application for high availability and fault tolerance? ►
Architecture
- Graceful shutdown โ stop accepting new connections on SIGTERM, complete in-flight requests, close DB/Redis connections, then exit.
- Health endpoints โ
/health/live(is process alive?) and/health/ready(can it accept traffic?). Kubernetes uses these for restart and traffic routing decisions. - Automatic restart โ use PM2, Docker restart policies, or Kubernetes deployments to restart crashed processes automatically.
- Circuit breakers โ prevent cascading failures when downstream services are degraded (opossum library).
- Timeouts everywhere โ set timeouts on all outbound HTTP calls, DB queries, and cache operations. Never wait indefinitely.
- Idempotent retries โ retry transient failures (network errors, 503s) with exponential backoff and jitter. Only retry idempotent operations.
- Bulkhead pattern โ isolate critical paths. Use separate HTTP connection pools for critical and non-critical downstream calls so a slow dependency doesn’t exhaust your pool.
- Structured logging + alerting โ log errors with request ID, user ID, and stack trace. Alert on error rate spikes, not individual errors.
// Retry with exponential backoff and jitter
async function withRetry(fn, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try { return await fn(); }
catch (err) {
if (attempt === maxRetries || !isTransient(err)) throw err;
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 100, 10000);
await new Promise(r => setTimeout(r, delay));
}
}
}
20 What is the Node.js AbortController and AbortSignal? ►
Async AbortController / AbortSignal (global since Node.js 15, stable since Node 16) provide a standard mechanism for cancelling async operations โ HTTP requests, stream pipelines, delays, and custom operations.
const { AbortController } = globalThis; // global since Node 16
// Cancel a fetch request after timeout
const controller = new AbortController();
const { signal } = controller;
// Auto-abort after 5 seconds
const timeoutId = setTimeout(() => controller.abort(new Error('Timeout')), 5000);
try {
const response = await fetch('https://api.example.com/data', { signal });
const data = await response.json();
clearTimeout(timeoutId);
return data;
} catch (err) {
if (err.name === 'AbortError') console.log('Request timed out');
else throw err;
}
// AbortSignal.timeout() โ shorthand for auto-aborting (Node 17.3+)
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
// Cancel stream pipeline
const ac = new AbortController();
await pipeline(
fs.createReadStream('input.txt'),
transform,
fs.createWriteStream('output.txt'),
{ signal: ac.signal }
);
// Cancel from another part of code: ac.abort();
// Custom cancellable operation
function delay(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(id);
reject(signal.reason || new Error('Aborted'));
});
});
}
21 What is the Node.js –watch flag and how does it work? ►
Tooling Node.js 18.11+ introduced a built-in --watch flag that restarts the process automatically when a watched file changes โ eliminating the need for nodemon in many development scenarios.
# Watch mode โ restart on file changes node --watch server.js # Watch a specific directory or file node --watch-path=./src --watch-path=./config server.js # Available since Node 18.11 โ stable since Node 20 # Node 22+ supports --watch for all built-in test runner too node --watch --test tests/*.test.js # Comparison: # nodemon โ feature-rich, configurable, mature, supports ext filtering # --watch โ built-in, zero config, great for simple projects and CI # nodemon still preferred for: # - Watching specific file extensions: nodemon --ext ts,json # - Running scripts before/after restart (nodemon events) # - Non-JS files that trigger a restart # - Delay before restarting: nodemon --delay 2 // Under the hood: --watch uses fs.watch() which uses OS-native // inotify (Linux), FSEvents (macOS), or ReadDirectoryChangesW (Windows) // for efficient file change detection without polling
📝 Knowledge Check
These questions mirror real senior-level Node.js architecture and internals interview scenarios.
🧠 Quiz Question 1 of 5
🧠 Quiz Question 2 of 5
🧠 Quiz Question 3 of 5
🧠 Quiz Question 4 of 5
🧠 Quiz Question 5 of 5
setImmediate runs before setTimeout(fn, 0) inside I/O callbacks. For V8, explain JIT compilation before deoptimisation. For libuv, explain why the thread pool exists (OS async limitations). Framing your answer as: limitation โ solution โ how Node.js implements it shows true depth.