NodeJSCommon Interview Questions

Common Interview Questions

These are the Node.js questions that appear frequently in technical interviews — from junior to senior level. Each answer goes beyond the surface definition to explain the why and the edge cases, which is what interviewers are actually testing for.

How does the Node.js event loop work?
Answer: single-threaded loop that cycles through phases; I/O callbacks are non-blocking via OS APIs; microtasks drain between each phase
Node.js uses a **single-threaded event loop** built on libuv. The loop cycles through phases: timers (setTimeout/setInterval), pending I/O callbacks, idle/prepare, poll (wait for I/O), check (setImmediate), and close events. Between every phase, microtasks run: first all `process.nextTick` callbacks, then Promise microtasks. Network I/O is truly non-blocking via OS APIs (epoll/kqueue/IOCP) — no threads needed. File I/O, DNS, and some crypto go through libuv's thread pool (default 4 threads). CPU-heavy work blocks the event loop — offload it to worker threads. The poll phase is where Node waits for new I/O; a healthy event loop spends most of its time there.
What is the difference between process.nextTick and setImmediate?

TS
process.nextTick(() => console.log('nextTick'))
setImmediate(() => console.log('setImmediate'))
Promise.resolve().then(() => console.log('Promise'))
nextTick
Promise
setImmediate
Answer: nextTick fires before Promises, both fire before setImmediate; nextTick can starve I/O if used recursively; setImmediate yields to I/O between calls
`process.nextTick` is **not part of the event loop phases** — its queue drains before the loop moves to the next phase (and before Promise microtasks). `setImmediate` runs in the check phase, after the poll phase has processed I/O. Practical consequence: use `setImmediate` for recursive async operations — a recursive `nextTick` starves the event loop and I/O callbacks never run. Use `process.nextTick` when you need to fire a callback **before any I/O** in the current iteration (e.g., emitting an error after construction, before any listeners could have been attached).
What is a memory leak in Node.js and how do you find one?
Answer: a growing object graph that GC can't collect due to retained references — diagnose with heap snapshots, fix by removing root references
A memory leak is an object (or set of objects) that the GC cannot collect because something still holds a reference to it, even though the application no longer needs it. Common causes: event listeners added with `on()` but never removed with `off()`; unbounded Maps/arrays acting as caches without eviction; closures capturing large objects in callbacks that live longer than expected; `setInterval` holding references forever. To find one: use `process.memoryUsage().heapUsed` to confirm the heap grows over time under load; take heap snapshots before and after load with `v8.writeHeapSnapshot()`; load both in Chrome DevTools Memory tab and compare "Objects allocated between snapshots" — the retainer chain shows what's keeping the object alive.
What are streams and why use them?
Answer: streams process data incrementally in chunks, keeping memory use constant regardless of data size; use them for large files, HTTP bodies, and any data too big to load in memory
A stream lets you process data piece by piece instead of loading it all into memory first. Reading a 10 GB file with `fs.readFileSync` allocates 10 GB of heap; reading it with `fs.createReadStream` keeps maybe 64 KB in memory at any time. Streams apply wherever data flows: HTTP request/response bodies, file I/O, database result sets, compression, encryption. Node streams are composable — you can pipe a readable through a transform (gzip, crypto) into a writable, and backpressure is handled automatically: if the writable is slow, the readable pauses. The `pipeline()` helper from `node:stream/promises` is the idiomatic way to compose streams and handles error propagation and cleanup.
What is the difference between CommonJS and ES modules?

Aspect

CommonJS

ES Modules

Syntax

require() / module.exports

import / export

Loading

Synchronous, evaluated on first require()

Async (parsed and linked before execution)

__dirname

Available as a global

Use import.meta.url + fileURLToPath

Top-level await

Not supported

Supported

File extension

.js or .cjs

.mjs or .js with "type":"module"

Interop

Can require() most CJS from CJS

Can import CJS; CJS cannot require() ESM

Tree-shaking

Difficult (dynamic)

Supported (static analysis)

How do you handle uncaught exceptions and unhandled rejections?

TS
// The correct pattern: log the error, then exit and let the supervisor restart:
process.on('uncaughtException', (err) => {
  logger.fatal({ err }, 'Uncaught exception')
  process.exit(1)
})

process.on('unhandledRejection', (reason) => {
  logger.fatal({ reason }, 'Unhandled rejection')
  process.exit(1)
})

// ❌ Never swallow these errors:
process.on('uncaughtException', () => {})   // the process is in unknown state — bad idea
Answer: log the error with full context, then exit with code 1 — the process is in an unknown state after an uncaught exception; a clean restart is safer than continuing
After an uncaught exception, the process may be in an inconsistent state: half-written files, locked database connections, corrupted in-memory state. Continuing execution is likely to produce wrong results silently. The safe response is to **log everything you know** (error, stack trace, request context), then `process.exit(1)` to signal failure. Your process supervisor (PM2, Kubernetes, systemd) will restart the process cleanly. `unhandledRejection` became fatal by default in Node 15 — if you're on Node 14 or lower, you need this handler explicitly to prevent silent failures.
What is the difference between authentication and authorization?
Answer: authentication establishes identity (who are you?); authorization determines permissions (what can you do?)
**Authentication** verifies *who* the caller is: validate a JWT, check a session token, verify an API key. **Authorization** decides *what* they can do: does this user have the admin role? Does this user own the resource they're trying to modify? The common pattern in Express middleware: an `authenticate` middleware runs first and attaches the verified identity to `req.user`; an `authorize(role)` middleware checks `req.user.role`. They're separate because authentication runs once per request, while authorization is checked at each protected route with different requirements.
What is backpressure in streams?
Answer: backpressure is the signal from a slow consumer to a fast producer to slow down — Node streams handle it via the return value of write() and the 'drain' event
Backpressure is the mechanism that prevents a fast producer from overwhelming a slow consumer. When you write to a Writable stream, `write()` returns `false` if the internal buffer is full — this is the signal to pause the Readable. When the buffer drains, the Writable emits `'drain'` and you resume reading. `pipe()` and `pipeline()` handle this automatically — they pause the Readable when `write()` returns false and resume on `'drain'`. Without backpressure handling, a slow disk write combined with a fast network read could buffer gigabytes in memory. This is why you should always use `pipeline()` rather than manually chaining `.on('data')` events.
What is the cluster module and when do you use it?
Answer: cluster spawns multiple Node processes on the same machine to use all CPU cores; each worker handles requests independently with shared port; use in production for multi-core utilization
Node is single-threaded — a single process uses only one CPU core. The `cluster` module forks multiple worker processes, each with its own event loop and V8 heap, all listening on the same port (the OS distributes incoming connections). The primary process manages workers and restarts crashed ones. In practice, most production deployments use multiple single-process containers in Kubernetes rather than a clustered single container — the end result is the same (multiple Node processes) but containers give better isolation and independent scaling. Use the cluster module for bare-metal or VM deployments where you control the host. With PM2, `pm2 start app.js -i max` handles clustering automatically.
What is the difference between SQL and NoSQL databases?

Dimension

SQL (PostgreSQL, MySQL)

NoSQL (MongoDB, DynamoDB, Redis)

Schema

Fixed schema, enforced by DB

Flexible (document/key-value/graph)

Relationships

First-class: JOINs, foreign keys

Application-managed references

ACID transactions

Yes — across multiple tables

Varies; MongoDB has multi-doc transactions; many NoSQL don't

Scaling

Vertical + read replicas (horizontal harder)

Horizontal sharding built in

Query language

SQL — powerful, standardized

Varies by database

Best for

Complex relationships, financial data, reporting

High write throughput, simple access patterns, unstructured data

Next
Apply your knowledge: [Project Ideas to Build](/nodejs/project-ideas).