Go2X
Go2X

India's leading training and placement platform offering hands-on learning, powered by 200+ IITian and industry experts, connecting students to 1,000+ hiring and referral partners.

Let's Go2X

Stay updated with Go2X

Get course updates, interview tips, and career insights delivered to your inbox.

Contact Us

Address

1st Floor, Plot No 332, Phase IV, Udyog Vihar,
Sector 19, Gurugram, Haryana 122015

Email

support@go2x.live

Phone

+91 94107 10085

© 2025 Go2X Private Limited. All rights reserved.

Made with 🧡 and a lot of late nights.

Interview ExperiencesBlogsAbout UsPrivacy PolicyTerms of ServiceRefund Policy

On This Page:

Backend

Node.js Interview Questions

Master your Node.js interviews with the most asked questions for freshers and experienced developers. Covers Node.js concepts, event loop, concurrency, memory management, and real-world scenarios.

July 28, 2026
38 mins read

I. Beginner Level

1. What is Node.js and how does it differ from browser JavaScript?

Node.js is an open-source, cross-platform JavaScript runtime built on Chrome's V8 engine. It lets you run JavaScript on the server - outside the browser. It was created by Ryan Dahl in 2009 to solve the problem of building highly concurrent I/O-bound servers without relying on threads.

FeatureBrowser JavaScriptNode.js
EngineV8, SpiderMonkey, JavaScriptCoreV8 only
DOM accessYes (window, document, DOM APIs)No - not a browser environment
File systemNo - sandboxed for securityYes - via the built-in fs module
NetworkingLimited (fetch, WebSockets)Full TCP/UDP/HTTP server support
Global objectwindowglobal (or globalThis)
Module systemES Modules nativeCommonJS (default) + ES Modules
Use caseUI, user interaction, animationsServers, REST APIs, CLIs, scripts

2. What is the V8 engine and what role does it play in Node.js?

V8 is an open-source JavaScript engine written in C++ and developed by Google. It is the engine that powers both Google Chrome and Node.js. V8's job is to take your JavaScript source code and execute it as fast as possible.

  • JIT compilation: V8 compiles JavaScript to native machine code at runtime (Just-In-Time), rather than interpreting it line by line. This is why JavaScript can be very fast despite being a dynamically typed language.

  • Garbage collection: V8 manages memory automatically using a generational garbage collector. Objects that are no longer referenced are collected and their memory is freed.

  • Ignition + TurboFan: V8's internal pipeline - Ignition is the interpreter that runs code initially. TurboFan is the optimising compiler that watches hot code paths and recompiles them into highly optimised machine code.

  • Role in Node.js: Node.js embeds V8 and adds capabilities around it - file system access, networking, timers, and the event loop (via libuv). V8 handles only JavaScript execution; everything else is Node.js or libuv.

3. Node.js is single-threaded - does that mean it can only handle one request at a time?

No - and this is one of the most misunderstood things about Node.js. Node.js has a single JavaScript execution thread, but it can handle thousands of concurrent requests thanks to its non-blocking, event-driven model.

When Node.js hits an I/O operation (database query, file read, HTTP call), it hands that work off to the OS or libuv's thread pool and immediately moves on to handle the next request. When the I/O work completes, its callback is placed in the event queue and the event loop picks it up.

javascript
1// Node.js can handle 1000 concurrent DB queries despite being single-threaded
2// because it doesn't WAIT for each query - it fires all 1000 and handles
3// responses as they come back
4
5const http = require('http');
6
7http.createServer(async (req, res) => {
8  // This DB call is non-blocking - Node doesn't sit and wait
9  // It registers the callback and goes back to the event loop
10  const user = await db.findUser(req.url.split('/')[2]);
11  res.end(JSON.stringify(user));
12}).listen(3000);
13
14// While one request is waiting for the DB, Node.js is already
15// accepting and processing other incoming requests
16

4. What is npm and what is the difference between dependencies and devDependencies?

npm (Node Package Manager) is the default package manager for Node.js. It lets you install, update, and manage third-party libraries (packages) for your project. It comes bundled with Node.js automatically.

FeaturedependenciesdevDependencies
PurposePackages needed to run the app in productionPackages needed only during development or testing
Install commandnpm install expressnpm install jest --save-dev
Installed in production?Yes - always installedNo - skipped with npm install --production
Examplesexpress, mongoose, axios, bcryptjest, eslint, nodemon, typescript

5. How does the CommonJS module system work in Node.js?

CommonJS (CJS) is the original module system in Node.js. Every file is treated as a separate module with its own scope. You use require() to load a module and module.exports to expose things from it. Modules are loaded synchronously and cached after the first load.

javascript
1// math.js - exporting
2const add      = (a, b) => a + b;
3const subtract = (a, b) => a - b;
4
5module.exports = { add, subtract };   // export an object
6// OR: module.exports = add;          // export a single value
7
8// app.js - importing
9const { add, subtract } = require('./math');  // local file
10const fs      = require('fs');                // built-in core module
11const express = require('express');           // npm package
12
13console.log(add(2, 3));       // 5
14console.log(subtract(10, 4)); // 6
15
16// Each file gets these variables automatically in CommonJS:
17// __filename - absolute path to the current file
18// __dirname  - absolute path to the current file's directory
19// module     - the module object
20// exports    - shorthand for module.exports
21// require    - the function to load modules
22

6. What is package.json and what are its most important fields?

package.json is the manifest file for a Node.js project. It lives at the root of the project and describes the project's identity, dependencies, scripts, and configuration. Running npm init creates it. Without it, npm doesn't know anything about your project.

json
1{
2  "name": "my-api",
3  "version": "1.0.0",
4  "description": "A sample REST API",
5  "main": "src/index.js",
6  "scripts": {
7    "start":   "node src/index.js",
8    "dev":     "nodemon src/index.js",
9    "test":    "jest --coverage",
10    "build":   "tsc"
11  },
12  "engines": {
13    "node": ">=18.0.0"
14  },
15  "dependencies": {
16    "express":  "^4.18.2",
17    "mongoose": "^8.0.0"
18  },
19  "devDependencies": {
20    "nodemon": "^3.0.0",
21    "jest":    "^29.0.0"
22  }
23}
24
  • name + version: Unique identifier for the package. Required if you plan to publish to npm.

  • main: The entry point file Node.js loads when the package is required. Defaults to index.js.

  • scripts: Shortcut commands run with npm run <name>. npm start and npm test are special and don't need the run keyword.

  • engines: Declares which Node.js versions your app is compatible with - useful for CI/CD and deployment validation.

7. What are the global objects available in Node.js?

Global objects in Node.js are available everywhere without requiring an import. They are part of the global scope (similar to how window works in the browser). Knowing what is truly global vs what is module-scoped is a common interview trap.

Global ObjectDescription
globalThe top-level object (equivalent of window in browser). Variables declared with var at the top of a file are NOT added to global in Node.js (they're module-scoped).
processInfo about the current process - process.env (env vars), process.argv (CLI args), process.exit(), process.pid, process.cwd().
consoleconsole.log(), console.error(), console.warn(), console.table(), console.time().
BufferClass for working with binary data. Available globally without require().
setTimeout / setInterval / setImmediate / clearTimeout / clearIntervalTimer functions - globally available without require().
__filename (module scope)Absolute path to the current file. NOT truly global - injected per module by CommonJS wrapper.
__dirname (module scope)Absolute path to the current file's directory. Also module-scoped, not a true global.

8. How do you create a basic HTTP server using Node.js's built-in http module?

Node.js ships with a built-in http module that lets you create an HTTP server from scratch without any third-party packages. Understanding how to use it directly is important because frameworks like Express.js are built on top of it.

javascript
1const http = require('http');
2
3const server = http.createServer((req, res) => {
4  // req = IncomingMessage - the incoming HTTP request
5  // res = ServerResponse  - the outgoing HTTP response
6
7  console.log(`${req.method} ${req.url}`);
8
9  // Simple router
10  if (req.url === '/' && req.method === 'GET') {
11    res.writeHead(200, { 'Content-Type': 'application/json' });
12    res.end(JSON.stringify({ message: 'Hello from Node.js!' }));
13
14  } else if (req.url === '/health') {
15    res.writeHead(200, { 'Content-Type': 'application/json' });
16    res.end(JSON.stringify({ status: 'ok' }));
17
18  } else {
19    res.writeHead(404, { 'Content-Type': 'application/json' });
20    res.end(JSON.stringify({ error: 'Route not found' }));
21  }
22});
23
24const PORT = process.env.PORT || 3000;
25server.listen(PORT, () => {
26  console.log(`Server running at http://localhost:${PORT}`);
27});
28
29// This is essentially what Express does internally - it just
30// adds routing, middleware, and body parsing on top
31

9. What is the fs module in Node.js and what are its common methods?

The fs (File System) module is a built-in Node.js module for interacting with the file system - reading files, writing files, creating/deleting directories, and more. Each operation has both a synchronous version (blocks the event loop) and an asynchronous version (non-blocking). In production server code always use the async versions.

javascript
1const fs = require('fs');
2const fsp = require('fs/promises'); // promise-based API (Node 14+)
3
4// ─── Async callback style ────────────────────────────────────────────────
5fs.readFile('./data.json', 'utf8', (err, data) => {
6  if (err) return console.error(err);
7  console.log(JSON.parse(data));
8});
9
10fs.writeFile('./output.txt', 'Hello Node!', 'utf8', (err) => {
11  if (err) throw err;
12  console.log('File written successfully');
13});
14
15// ─── Promise / async-await style (preferred) ─────────────────────────────
16async function readConfig() {
17  const data = await fsp.readFile('./config.json', 'utf8');
18  return JSON.parse(data);
19}
20
21async function saveLog(message) {
22  await fsp.appendFile('./app.log', `${new Date().toISOString()} - ${message}\n`);
23}
24
25// Other common fs methods:
26// fs.existsSync(path)              - check if file/dir exists (sync is ok for startup)
27// fs.mkdirSync(path, { recursive: true }) - create directory tree
28// fs.readdirSync(path)             - list files in a directory
29// fs.unlinkSync(path)              - delete a file
30// fs.rename(oldPath, newPath, cb)  - rename / move a file
31// fs.statSync(path)                - get file metadata (size, modified date, etc.)
32// fs.createReadStream(path)        - read large files as a stream
33// fs.createWriteStream(path)       - write large files as a stream
34

10. What is the path module and why should you use it instead of string concatenation for file paths?

The path module is a built-in Node.js module for working with file and directory paths. The core reason to use it instead of manual string concatenation is cross-platform compatibility - Windows uses backslashes (\) while Linux/macOS use forward slashes (/). path methods handle this automatically.

javascript
1const path = require('path');
2
3// ✗ Bad - breaks on Windows
4const configPath = __dirname + '/config/database.json';
5
6// ✅ Good - works on all platforms
7const configPath = path.join(__dirname, 'config', 'database.json');
8
9// ─── Common path methods ─────────────────────────────────────────────────
10
11// path.join() - joins path segments with the correct separator
12path.join('/home', 'vishal', 'app', 'index.js');
13// '/home/vishal/app/index.js'
14
15// path.resolve() - resolves to an absolute path from left to right
16path.resolve('src', 'routes', 'user.js');
17// '/current/working/directory/src/routes/user.js'
18
19// path.basename() - filename from a path (with or without extension)
20path.basename('/home/vishal/app/index.js');       // 'index.js'
21path.basename('/home/vishal/app/index.js', '.js'); // 'index'
22
23// path.dirname() - directory part of a path
24path.dirname('/home/vishal/app/index.js'); // '/home/vishal/app'
25
26// path.extname() - file extension
27path.extname('photo.png');  // '.png'
28path.extname('archive.tar.gz'); // '.gz'
29
30// path.parse() - breaks a path into its components
31path.parse('/home/vishal/app/index.js');
32// { root: '/', dir: '/home/vishal/app', base: 'index.js',
33//   ext: '.js', name: 'index' }
34

11. What is the difference between blocking and non-blocking code in Node.js?

Blocking code holds up the event loop - while it executes, no other callbacks or requests can run. Non-blocking code returns immediately and schedules a callback for when the work is done, freeing the event loop to handle other work in the meantime.

javascript
1const fs = require('fs');
2
3// ✗ BLOCKING - freezes the server for all users until the file is read
4const data = fs.readFileSync('/large-file.txt', 'utf8');
5console.log('File read complete');
6console.log('This runs AFTER file is fully read');
7
8// ✅ NON-BLOCKING - server keeps serving requests while file is being read
9fs.readFile('/large-file.txt', 'utf8', (err, data) => {
10  if (err) throw err;
11  console.log('File read complete'); // runs later, when OS is done
12});
13console.log('This runs IMMEDIATELY - before the file is read');
14
15// ✅ NON-BLOCKING with async/await (cleaner)
16const { readFile } = require('fs/promises');
17async function loadFile() {
18  const data = await readFile('/large-file.txt', 'utf8'); // non-blocking
19  return data;
20}
21
BlockingNon-Blocking
Halts the event loop until completeReturns immediately, callback fires later
All other requests wait in queueEvent loop handles other requests concurrently
Examples: fs.readFileSync, crypto.pbkdf2SyncExamples: fs.readFile, http.get, db.query
OK for startup scripts and one-off CLIsRequired for production web servers

12. What is the error-first callback pattern in Node.js?

The error-first callback (also called Node-style callback or errback) is a convention where the first argument of every async callback is reserved for an error object. If the operation succeeded, the first argument is null. If it failed, it contains the Error. This was Node.js's original way of handling async errors before Promises.

javascript
1const fs = require('fs');
2
3// The callback always follows the pattern: (err, result)
4fs.readFile('./config.json', 'utf8', (err, data) => {
5  // ALWAYS check err first
6  if (err) {
7    console.error('Failed to read file:', err.message);
8    return; // stop execution - don't use data
9  }
10  // Only reach here if err is null
11  console.log(JSON.parse(data));
12});
13
14// Writing your own error-first callback functions
15function divideNumbers(a, b, callback) {
16  if (b === 0) {
17    // Error case - pass an Error as the first argument
18    return callback(new Error('Cannot divide by zero'));
19  }
20  // Success case - first arg is null, second is the result
21  callback(null, a / b);
22}
23
24divideNumbers(10, 2, (err, result) => {
25  if (err) return console.error(err.message);
26  console.log('Result:', result); // 5
27});
28
29divideNumbers(10, 0, (err, result) => {
30  if (err) return console.error(err.message); // 'Cannot divide by zero'
31  console.log(result); // never reached
32});
33

13. What is the EventEmitter class in Node.js and how is it used?

EventEmitter is the core publish-subscribe pattern in Node.js, found in the events module. Nearly everything in Node.js (streams, http.Server, child processes) extends EventEmitter. It allows decoupled components to communicate - one part emits a named event, another part listens for it.

javascript
1const EventEmitter = require('events');
2
3const emitter = new EventEmitter();
4
5// on() - subscribe to an event (fires every time event is emitted)
6emitter.on('data', (chunk) => console.log('Received:', chunk));
7
8// once() - subscribe but fires only the first time
9emitter.once('connect', () => console.log('Connected!'));
10
11// emit() - publish / trigger an event
12emitter.emit('data', { id: 1, value: 'hello' });
13emitter.emit('connect');
14emitter.emit('connect'); // ignored - once() already fired
15
16// off() - remove a specific listener (important for memory leak prevention)
17const handler = (data) => console.log(data);
18emitter.on('update', handler);
19emitter.off('update', handler); // removes that specific handler
20
21// Custom class extending EventEmitter
22class OrderService extends EventEmitter {
23  async placeOrder(orderData) {
24    const order = await db.createOrder(orderData);
25    this.emit('order:created', order);   // notify listeners
26    this.emit('inventory:update', order.items);
27    return order;
28  }
29}
30
31const orderService = new OrderService();
32orderService.on('order:created', order => sendConfirmationEmail(order));
33orderService.on('inventory:update', items => updateInventory(items));
34
35// ⚠️ 'error' event is special - if emitted with no listener, Node.js throws
36emitter.on('error', (err) => console.error('Caught:', err.message)); // always add this
37

II. Intermediate Level

1. Explain the Node.js event loop in detail.

The event loop is the heart of Node.js's non-blocking I/O model. Node.js has one call stack. When it hits something slow (a file read, DB query, HTTP call), it offloads that work to libuv's thread pool or the OS. It then keeps executing other code. When the slow work completes, its callback enters a queue. The event loop continuously checks: is the call stack empty? If yes, pull the next callback from the queue and push it onto the stack.

javascript
1console.log('1 - start');          // sync - runs immediately
2
3setTimeout(() => {
4  console.log('4 - setTimeout');   // macrotask queue
5}, 0);
6
7Promise.resolve().then(() => {
8  console.log('3 - promise');       // microtask queue
9});
10
11process.nextTick(() => {
12  console.log('2 - nextTick');      // nextTick queue (highest priority)
13});
14
15console.log('5 - end');            // sync - runs immediately
16
17// Output:
18// 1 - start
19// 5 - end
20// 2 - nextTick    ← nextTick drains before anything else
21// 3 - promise     ← Promise microtasks drain next
22// 4 - setTimeout  ← macrotask fires last
23

2. What are the phases of the Node.js event loop?

The event loop runs through a fixed sequence of phases on each iteration. Each phase has its own FIFO queue of callbacks. After every phase transition, Node.js completely drains the nextTick queue and the Promise microtask queue before moving forward.

PhaseWhat runs hereExample
1. timersCallbacks from setTimeout() and setInterval() whose delay has expiredsetTimeout(fn, 100)
2. pending callbacksI/O callbacks deferred from the previous loop (e.g. TCP errors)TCP socket errors
3. idle, prepareInternal Node.js use only - not exposed to userland-
4. pollRetrieves new I/O events. Executes I/O callbacks. Blocks here waiting for I/O if no timers are pending.fs.readFile callback
5. checksetImmediate() callbacks - always runs after the poll phasesetImmediate(fn)
6. close callbacksCleanup callbacks for abruptly closed handlessocket.destroy() → 'close' event

3. What is the difference between process.nextTick(), setImmediate(), and setTimeout()?

All three schedule code to run asynchronously, but at very different points in the event loop. The names are deliberately misleading - nextTick doesn't run on the next tick, and setImmediate isn't always immediate.

APIWhen it runsUse case
process.nextTick()After current operation, before any I/O or timer - highest priorityGuarantee callback runs before any I/O. Propagate errors. Emit events after object construction.
Promise.then()After nextTick queue drains, still before I/OGeneral async operations with clean, chainable syntax
setImmediate()Check phase - after poll/I/O phase completesRun after I/O callbacks. Preferred over setTimeout(0) inside I/O handlers.
setTimeout(fn, 0)Timers phase - minimum 1ms delay, not guaranteed to be 0msDeferred execution. Ordering vs setImmediate is non-deterministic outside I/O.
javascript
1const fs = require('fs');
2
3// Inside an I/O callback, order is always deterministic
4fs.readFile(__filename, () => {
5  setTimeout(    () => console.log('setTimeout'),  0);
6  setImmediate(  () => console.log('setImmediate'));
7  process.nextTick(() => console.log('nextTick'));
8  Promise.resolve().then(() => console.log('promise'));
9});
10// Output (always):
11// nextTick → promise → setImmediate → setTimeout
12

4. What are microtasks and macrotasks in Node.js?

Node.js categorises async callbacks into two groups. The key rule: the entire microtask queue is completely drained after every phase of the event loop before moving to the next phase. A microtask can schedule another microtask and it runs immediately - before any macrotask.

TypeIncludesPriority
Microtasksprocess.nextTick(), Promise.then/catch/finally, queueMicrotask()Highest - entire queue drained before next event loop phase
MacrotaskssetTimeout(), setInterval(), setImmediate(), I/O callbacks, close callbacksLower - one per event loop phase iteration
javascript
1// ⚠️ Microtask starvation - a dangerous pattern
2function recursiveNextTick() {
3  process.nextTick(recursiveNextTick);
4}
5recursiveNextTick();
6// The nextTick queue never empties → event loop never advances
7// setTimeout and I/O will NEVER run - the server is starved
8
9// ✅ Safe recursive async pattern
10function recursiveSafe() {
11  setImmediate(recursiveSafe); // yields to event loop each iteration
12}
13

5. What is the difference between CommonJS and ES Modules in Node.js?

CommonJS is Node.js's original module system - synchronous and dynamic. ES Modules (ESM) are the official JavaScript standard - asynchronous and static. They have fundamentally different loading mechanisms that affect everything from treeshaking to top-level await.

FeatureCommonJSES Modules
Syntaxrequire() / module.exportsimport / export
LoadingSynchronous - blocks until module is loadedAsynchronous - supports top-level await
Import timingDynamic - require() can be inside conditions or loopsStatic - imports hoisted and resolved at parse time
Tree shakingNot possible - dynamic nature prevents static analysisPossible - bundlers can remove unused exports
__dirname / __filenameAvailable automaticallyNot available - use import.meta.url instead
File extension.js (default).mjs or "type": "module" in package.json
javascript
1// CommonJS
2const { readFile } = require('fs');
3module.exports = { myFunction };
4
5// Conditional import - valid in CJS
6if (process.env.DEBUG) {
7  const debugTools = require('./debug');
8}
9
10// ES Modules
11import { readFile } from 'fs';
12export { myFunction };
13export default myFunction;
14
15// ESM __dirname equivalent
16import { fileURLToPath } from 'url';
17import { dirname } from 'path';
18const __filename = fileURLToPath(import.meta.url);
19const __dirname  = dirname(__filename);
20
21// Dynamic import - works in both CJS and ESM
22const module = await import('./lazy-module.mjs');
23

6. How does module caching work in Node.js?

When you require() a module for the first time, Node.js loads, compiles, and executes it, then stores the exports object in require.cache keyed by the file's absolute path. Every subsequent require() of the same file returns the cached exports - the module code does not run again. This makes modules effectively singletons.

javascript
1// counter.js
2let count = 0;
3module.exports = { increment: () => ++count, getCount: () => count };
4
5// app.js
6const a = require('./counter');
7const b = require('./counter'); // returns cached object - NOT a new one
8
9a.increment();
10a.increment();
11console.log(b.getCount()); // 2 - same instance!
12console.log(a === b);      // true
13
14// Inspect the cache
15console.log(Object.keys(require.cache)); // all cached module paths
16
17// Force reload (rare - usually signals a design problem)
18delete require.cache[require.resolve('./counter')];
19const fresh = require('./counter'); // module code runs again
20

7. What are streams in Node.js? Explain the types.

Streams let you read or write data piece by piece (in chunks) instead of loading everything into memory at once. They are essential for large files, network data, or any data that arrives over time. All streams extend EventEmitter.

TypeDescriptionKey EventsExample
ReadableSource - you read data from itdata, end, errorfs.createReadStream(), http.IncomingMessage
WritableDestination - you write data to itdrain, finish, errorfs.createWriteStream(), http.ServerResponse
DuplexBoth readable and writable - independent sidesdata, end, drain, finishnet.Socket, TCP connections
TransformDuplex that modifies data as it passes throughdata, end, drain, finishzlib.createGzip(), crypto.createCipher()
javascript
1const fs   = require('fs');
2const zlib = require('zlib');
3const { pipeline } = require('stream/promises');
4
5// ✗ Loads entire file into memory - bad for large files
6const data = fs.readFileSync('huge.csv');
7fs.writeFileSync('huge.csv.gz', zlib.gzipSync(data));
8
9// ✅ Pipe streams - memory stays constant no matter how large the file
10await pipeline(
11  fs.createReadStream('huge.csv'),  // Readable
12  zlib.createGzip(),                // Transform (compress)
13  fs.createWriteStream('huge.csv.gz') // Writable
14);
15// pipeline() automatically destroys all streams on error - use it over .pipe()
16

8. What is a Buffer in Node.js and why is it used?

A Buffer is a fixed-size chunk of memory allocated outside the V8 heap for holding raw binary data. JavaScript strings are UTF-16, but files, network protocols, images, and encryption all work with raw bytes - that is what Buffer is for. It is available globally in Node.js without any require().

javascript
1// Creating buffers
2const buf1 = Buffer.alloc(10);               // 10 zero bytes (safe - always use this)
3const buf2 = Buffer.from('Hello Node.js');   // from a string (UTF-8)
4const buf3 = Buffer.from([0x48, 0x65, 0x6c]); // from a byte array
5
6// Reading
7console.log(buf2.toString());        // 'Hello Node.js'
8console.log(buf2.toString('hex'));   // '48656c6c6f...'
9console.log(buf2.length);           // byte length (not character count)
10
11// Buffers appear automatically in Node.js I/O
12const http = require('http');
13http.createServer((req, res) => {
14  const chunks = [];
15  req.on('data', chunk => chunks.push(chunk)); // each chunk is a Buffer
16  req.on('end', () => {
17    const body = Buffer.concat(chunks).toString();
18    res.end(body);
19  });
20}).listen(3000);
21
22// ⚠️ Never use Buffer.allocUnsafe() with user-facing data
23// It contains old (potentially sensitive) memory contents
24const unsafe = Buffer.allocUnsafe(10); // fast but uninitialized
25const safe   = Buffer.alloc(10);       // zero-filled - always prefer this
26

9. What are the differences between callbacks, Promises, and async/await?

These are three generations of handling asynchronous operations in Node.js. Each solves the readability and error-handling problems of the previous approach. All three are still found in real codebases.

FeatureCallbacksPromisesasync/await
Error handlingFirst argument (err, data).catch() or second .then() argtry/catch block
ChainingNested - leads to callback hell.then().then() flat chainSequential await - reads like sync code
Parallel tasksManual counter or async libraryPromise.all()await Promise.all()
DebuggingStack traces lose contextImprovedBest - sync-style stack traces
javascript
1// Same task in all three styles: read a file then query a DB
2
3// 1. Callbacks
4fs.readFile('config.json', (err, data) => {
5  if (err) return handleError(err);
6  db.query('SELECT * FROM users', (err2, rows) => {
7    if (err2) return handleError(err2);
8    console.log(rows);
9  });
10});
11
12// 2. Promises
13fs.promises.readFile('config.json')
14  .then(() => db.query('SELECT * FROM users'))
15  .then(rows => console.log(rows))
16  .catch(handleError);
17
18// 3. async/await
19async function loadData() {
20  try {
21    await fs.promises.readFile('config.json');
22    const rows = await db.query('SELECT * FROM users');
23    console.log(rows);
24  } catch (err) {
25    handleError(err);
26  }
27}
28

10. What is callback hell and how do you avoid it?

Callback hell (the pyramid of doom) is when deeply nested callbacks make code hard to read, maintain, and debug. It happens when async operations depend on each other's results, causing nesting that grows horizontally.

javascript
1// ✗ Callback hell
2getUser(id, (err, user) => {
3  if (err) return handleError(err);
4  getOrders(user.id, (err, orders) => {
5    if (err) return handleError(err);
6    getDetails(orders[0].id, (err, details) => {
7      if (err) return handleError(err);
8      sendEmail(user.email, details, (err) => {
9        if (err) return handleError(err);
10        console.log('Done!'); // buried 4 levels deep
11      });
12    });
13  });
14});
15
16// ✅ async/await - reads like synchronous code
17async function processOrder(id) {
18  try {
19    const user    = await getUser(id);
20    const orders  = await getOrders(user.id);
21    const details = await getDetails(orders[0].id);
22    await sendEmail(user.email, details);
23    console.log('Done!');
24  } catch (err) {
25    handleError(err);
26  }
27}
28

11. What is the difference between Promise.all(), Promise.allSettled(), Promise.race(), and Promise.any()?

All four accept an array of Promises and run them in parallel, but differ in how they handle resolution and rejection. Choosing the right one has a big impact on both performance and error handling.

MethodResolves whenRejects whenBest for
Promise.all()ALL promises resolveANY one promise rejects (fast-fail)Parallel tasks where ALL must succeed
Promise.allSettled()ALL promises settle (either way)Never - always resolvesBatch operations - need results even if some fail
Promise.race()FIRST promise to settle (resolve or reject)First settled promise is a rejectionTimeout patterns - race a fetch vs a timer
Promise.any()FIRST promise to resolveALL promises reject (AggregateError)Try multiple sources - use the fastest success
javascript
1// Promise.all - fails if any one fails
2const [user, orders] = await Promise.all([fetchUser(id), fetchOrders(id)]);
3
4// Promise.allSettled - always resolves, inspect each result
5const results = await Promise.allSettled([api1(), api2(), api3()]);
6results.forEach(r => r.status === 'fulfilled'
7  ? console.log('OK:', r.value)
8  : console.error('Failed:', r.reason));
9
10// Promise.race - implement a request timeout
11const withTimeout = (p, ms) => Promise.race([
12  p,
13  new Promise((_, rej) => setTimeout(() => rej(new Error('Timeout')), ms)),
14]);
15const data = await withTimeout(fetchData(), 5000);
16
17// Promise.any - first successful CDN wins
18const img = await Promise.any([
19  fetch('https://cdn1.example.com/img.png'),
20  fetch('https://cdn2.example.com/img.png'),
21]);
22

12. How do you handle errors in async/await functions properly?

Error handling in async/await uses try/catch. The critical rule: an async function that throws without being caught creates an unhandled promise rejection - which crashes the process in Node.js 15+. Express also has a specific gotcha where async route handlers need a wrapper.

javascript
1// Pattern 1: try/catch
2async function getUser(id) {
3  try {
4    const user = await db.findById(id);
5    if (!user) throw new Error('User not found');
6    return user;
7  } catch (err) {
8    logger.error('getUser failed', { id, error: err.message });
9    throw err; // re-throw so caller knows
10  }
11}
12
13// Pattern 2: error wrapper - avoids repetitive try/catch
14const to = p => p.then(data => [null, data]).catch(err => [err, null]);
15
16async function handler(req, res) {
17  const [err, user] = await to(getUser(req.params.id));
18  if (err) return res.status(404).json({ error: err.message });
19  res.json(user);
20}
21
22// Pattern 3: Express async wrapper - catches async errors and forwards to error middleware
23const asyncHandler = fn => (req, res, next) =>
24  Promise.resolve(fn(req, res, next)).catch(next);
25
26app.get('/users/:id', asyncHandler(async (req, res) => {
27  const user = await getUser(req.params.id); // if throws, goes to error middleware
28  res.json(user);
29}));
30

13. What is an unhandledRejection and how do you handle it globally?

An unhandledRejection fires when a Promise is rejected and nothing catches it. Since Node.js 15, this terminates the process with a non-zero exit code. Every production Node.js app must handle both unhandledRejection and uncaughtException at the process level.

javascript
1// Add at the very top of your app entry file
2process.on('unhandledRejection', (reason, promise) => {
3  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
4  // Log to Sentry/Datadog, then exit
5});
6
7process.on('uncaughtException', (err) => {
8  console.error('Uncaught Exception:', err);
9  process.exit(1); // MUST exit - state is unknown after this
10});
11
12// ─── Most common causes ──────────────────────────────────────────────────
13
14// 1. Missing await
15async function bad() { fetchData(); } // ✗ no await
16async function good() { await fetchData(); } // ✅
17
18// 2. .then() without .catch()
19somePromise.then(process);               // ✗
20somePromise.then(process).catch(logErr); // ✅
21
22// 3. async event handler - EventEmitter doesn't catch async errors
23emitter.on('data', async (chunk) => { await processChunk(chunk); });          // ✗
24emitter.on('data', (chunk) => processChunk(chunk).catch(err => emitter.emit('error', err))); // ✅
25

14. How do you manage environment variables in Node.js?

Environment variables separate configuration from code, letting apps behave differently across dev, staging, and production without code changes. They are accessed via process.env and should never be hardcoded or committed to version control.

javascript
1// .env file - never commit to git (.gitignore it)
2// DATABASE_URL=postgres://user:pass@localhost:5432/mydb
3// JWT_SECRET=super-secret-key
4// PORT=3000
5// NODE_ENV=development
6
7require('dotenv').config(); // loads .env into process.env
8
9const port  = process.env.PORT || 3000;
10const dbUrl = process.env.DATABASE_URL;
11
12// ✅ Validate required vars at startup - fail fast with a clear message
13function validateEnv() {
14  const required = ['DATABASE_URL', 'JWT_SECRET'];
15  const missing  = required.filter(k => !process.env[k]);
16  if (missing.length) throw new Error(`Missing env vars: ${missing.join(', ')}`);
17}
18validateEnv(); // crash immediately if anything is missing
19
20// Use envalid for typed, validated env vars
21const { cleanEnv, str, port: portVal } = require('envalid');
22const env = cleanEnv(process.env, {
23  DATABASE_URL: str(),
24  PORT:         portVal({ default: 3000 }),
25  NODE_ENV:     str({ choices: ['development', 'test', 'production'] }),
26});
27

15. What is the difference between Node.js and Express.js?

Node.js is the runtime - it gives JavaScript the ability to run on servers and provides core modules (http, fs, path, crypto). Express.js is a web framework that runs on top of Node.js. You can build HTTP servers with raw Node.js, but Express removes the boilerplate.

FeatureNode.js (raw http)Express.js
RoutingManual - parse url and method yourselfBuilt-in: app.get(), app.post(), app.put()
MiddlewareNo concept - write all logic in one handlerapp.use() - chainable middleware pipeline
Body parsingManual - collect data events from req streamexpress.json(), express.urlencoded()
Error handlingManual in every handlerCentralised 4-arg error middleware
BoilerplateHighMinimal - focus on your logic

16. What is middleware in Express.js and how does it work?

Middleware are functions that sit between the request and the response. Each middleware gets (req, res, next) - it can modify req/res, end the request cycle, or call next() to pass control forward. They execute in the order they are registered.

javascript
1const express = require('express');
2const app = express();
3
4// 1. Application-level - runs for every request
5app.use((req, res, next) => {
6  console.log(`${req.method} ${req.url}`);
7  next(); // MUST call next() or the request hangs forever
8});
9
10// 2. Built-in middleware
11app.use(express.json());            // parse JSON request bodies
12app.use(express.static('public'));  // serve static files
13
14// 3. Route-specific middleware
15const authenticate = (req, res, next) => {
16  const token = req.headers.authorization?.split(' ')[1];
17  if (!token) return res.status(401).json({ error: 'No token' });
18  try {
19    req.user = jwt.verify(token, process.env.JWT_SECRET);
20    next();
21  } catch { res.status(401).json({ error: 'Invalid token' }); }
22};
23
24app.get('/profile', authenticate, (req, res) => res.json(req.user));
25
26// 4. Error-handling middleware - 4 args, registered last
27app.use((err, req, res, next) => {
28  res.status(err.status || 500).json({ error: err.message });
29});
30
31// Flow: Request → logger → json parser → authenticate → route handler → Error handler → Response
32

III. Advanced Level

1. What is libuv and what role does it play in Node.js?

libuv is a C library that provides Node.js with its asynchronous I/O capabilities and the event loop implementation. V8 handles JavaScript execution. libuv handles everything else - timers, file system, networking, DNS, and the thread pool.

  • Event loop: The entire event loop is implemented inside libuv, not in V8 or Node.js itself.

  • Thread pool: libuv maintains a pool of 4 threads by default (configurable via UV_THREADPOOL_SIZE up to 1024) to handle blocking OS APIs - file I/O, DNS lookups, and crypto operations. This is how Node.js does non-blocking file reads even though the OS file API is inherently blocking.

  • Cross-platform abstraction: libuv hides the differences between epoll (Linux), kqueue (macOS), and IOCP (Windows) behind a single uniform API - which is why the same Node.js code runs on all platforms.

  • TCP/UDP networking uses OS async I/O directly (not the thread pool), which is why Node can handle thousands of concurrent connections on a single thread.

2. What are child processes in Node.js and what are the ways to create them?

Child processes let you run external commands or other Node.js scripts in separate OS processes. Since Node.js is single-threaded, child processes are how you parallelise CPU-heavy work, run shell commands, or interact with other programs.

MethodWhat it doesBest for
exec()Runs a shell command, buffers full outputShort commands where you need the full output at once
execFile()Runs a specific file without a shellSafer than exec() - no shell injection risk
spawn()Launches a process with streaming stdioLong-running processes or large/streaming output
fork()Spawns a new Node.js process with a built-in IPC channelRun another Node.js script and exchange messages with it
javascript
1const { exec, spawn, fork } = require('child_process');
2
3// exec - simple shell command, buffered output
4exec('ls -la', (err, stdout) => {
5  if (err) return console.error(err);
6  console.log(stdout);
7});
8
9// spawn - streaming output (better for large output)
10const ls = spawn('ls', ['-la', '/tmp']);
11ls.stdout.on('data', data => process.stdout.write(data));
12ls.on('close', code => console.log(`Exited: ${code}`));
13
14// fork - another Node.js file with IPC messaging
15const child = fork('./worker.js');
16child.send({ task: 'compute', data: [1, 2, 3] });
17child.on('message', result => console.log('Result:', result));
18
19// worker.js
20process.on('message', ({ data }) => {
21  process.send({ result: data.reduce((a, b) => a + b, 0) });
22});
23

3. What are Worker Threads in Node.js and when do you use them?

Worker Threads (node:worker_threads, stable since Node 12) run JavaScript in separate threads within the same process. Unlike child processes, workers can share memory via SharedArrayBuffer with near-zero overhead.

FeatureChild Process (fork)Worker Thread
MemorySeparate - each process has its own heapShared via SharedArrayBuffer
Startup costHigh - spawns a new OS processLow - new thread in the same process
CommunicationIPC - serialised JSON messagesMessageChannel or SharedArrayBuffer
Use caseRunning different programs or shell commandsCPU-intensive JS tasks - image processing, crypto, ML
javascript
1const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
2
3if (isMainThread) {
4  const worker = new Worker(__filename, { workerData: { n: 1_000_000 } });
5  worker.on('message', result => console.log('Sum:', result));
6  worker.on('error',   err    => console.error(err));
7  console.log('Main thread stays responsive while worker computes');
8} else {
9  // Worker thread - CPU-heavy work runs here without blocking the event loop
10  let sum = 0;
11  for (let i = 0; i < workerData.n; i++) sum += i;
12  parentPort.postMessage(sum);
13}
14// Without Workers, the for loop would block ALL incoming HTTP requests
15

4. What is the cluster module in Node.js?

The cluster module lets you fork multiple child processes (workers) that all share the same server port. It is the primary way to use all CPU cores on a multi-core server - since a single Node.js process can only use one core.

javascript
1const cluster = require('cluster');
2const http    = require('http');
3const os      = require('os');
4
5if (cluster.isPrimary) {
6  console.log(`Primary ${process.pid} - forking ${os.cpus().length} workers`);
7  for (let i = 0; i < os.cpus().length; i++) cluster.fork();
8
9  cluster.on('exit', (worker) => {
10    console.log(`Worker ${worker.process.pid} died - restarting`);
11    cluster.fork(); // auto-restart crashed workers
12  });
13} else {
14  http.createServer((req, res) => {
15    res.end(`Handled by worker PID ${process.pid}`);
16  }).listen(3000);
17  console.log(`Worker ${process.pid} ready`);
18}
19// On an 8-core machine: 8 Node.js processes all on port 3000
20// OS distributes connections across them with round-robin
21

5. What causes memory leaks in Node.js and how do you detect them?

A memory leak happens when objects that are no longer needed are still being referenced, preventing V8's garbage collector from freeing that memory. Over time, the process consumes more RAM until it crashes or degrades.

CauseDescriptionFix
Global variablesData assigned to global scope never gets collectedUse local scope; use WeakMap for caches
Listeners not removedemitter.on() holds references to closuresCall emitter.off() or removeAllListeners() when done
Unbounded cachesIn-memory caches that grow indefinitelyUse LRU cache with a max size and TTL expiry
Timers not clearedActive timers keep their closure aliveAlways clearTimeout/clearInterval in cleanup
Streams not consumedUnread readable streams buffer data indefinitelyAlways consume or destroy unused streams
javascript
1// Detection - monitor heap size over time
2setInterval(() => {
3  const { heapUsed } = process.memoryUsage();
4  console.log(`Heap: ${(heapUsed / 1024 / 1024).toFixed(1)} MB`);
5}, 5000);
6
7// Take a heap snapshot for Chrome DevTools analysis
8const v8 = require('v8');
9const file = v8.writeHeapSnapshot();
10console.log('Heap snapshot:', file); // open in Chrome DevTools → Memory tab
11
12// Use clinic.js for automated profiling
13// $ npm i -g clinic
14// $ clinic doctor -- node app.js
15
16// ✗ Classic unbounded cache leak
17const cache = {};
18app.get('/data/:id', async (req, res) => {
19  if (!cache[req.params.id]) cache[req.params.id] = await fetchData(req.params.id);
20  res.json(cache[req.params.id]); // cache grows forever!
21});
22
23// ✅ LRU cache with eviction
24const { LRUCache } = require('lru-cache');
25const cache = new LRUCache({ max: 500, ttl: 5 * 60 * 1000 }); // 500 items, 5 min TTL
26

6. What are the best practices for improving Node.js performance?

Node.js performance optimisation has two goals: keep the event loop free (never block it with CPU work) and minimise unnecessary overhead in your code, queries, and dependencies.

  • Never block the event loop: Avoid readFileSync, heavy computations, or large JSON.parse() on the critical path. Offload to Worker Threads.

  • Use clustering or PM2 cluster mode: Spin up one worker per CPU core so incoming connections are handled truly in parallel.

  • Stream large data: Use streams instead of loading entire files or response bodies into memory.

  • Use connection pooling: Create a pool of DB connections at startup (pg-pool, mongoose) - never create a new connection per request.

  • Cache aggressively: Redis for query caching, HTTP Cache-Control headers for clients, CDN for static assets. The fastest query is no query at all.

  • Enable gzip/brotli: Use compression middleware (or Nginx) to shrink text response sizes significantly.

  • Profile before optimising: Use clinic.js or 0x for flame graphs. Fix what the profiler shows is actually slow - not what you assume.

javascript
1// ✗ Blocking the event loop with heavy CPU work
2app.get('/fibonacci', (req, res) => {
3  const result = fibonacci(parseInt(req.query.n)); // freezes all other requests!
4  res.json({ result });
5});
6
7// ✅ Offload to Worker Thread
8const { Worker } = require('worker_threads');
9app.get('/fibonacci', (req, res) => {
10  const worker = new Worker('./fib-worker.js', { workerData: { n: parseInt(req.query.n) } });
11  worker.on('message', result => res.json({ result }));
12  worker.on('error',   err    => res.status(500).json({ error: err.message }));
13});
14
15// ✅ DB connection pool
16const { Pool } = require('pg');
17const pool = new Pool({ max: 20, idleTimeoutMillis: 30_000 });
18app.get('/users', async (req, res) => {
19  const { rows } = await pool.query('SELECT id, name FROM users LIMIT 50');
20  res.json(rows);
21});
22

7. What are the security best practices for a Node.js application?

Security covers your application code, HTTP headers, authentication, dependencies, and production configuration. Ignoring any one layer is enough for an attacker to exploit.

CategoryBest PracticeTool
DependenciesAudit and update packages regularlynpm audit, Snyk
HTTP HeadersSet CSP, HSTS, X-Frame-Options, X-Content-Type-Optionshelmet middleware
Rate limitingLimit requests per IP to prevent brute-force and DoSexpress-rate-limit, Redis rate limiter
Input validationValidate and sanitise all user input at the route levelzod, joi, express-validator
SecretsNever hardcode secrets - use environment variablesdotenv, AWS Secrets Manager
PasswordsHash with bcrypt (minimum 12 rounds) - never store plain textbcrypt
CORSWhitelist specific origins - never use wildcard *cors middleware with origin allowlist
javascript
1const helmet   = require('helmet');
2const rateLimit = require('express-rate-limit');
3const cors     = require('cors');
4const bcrypt   = require('bcrypt');
5
6app.use(helmet()); // sets 11 security headers in one line
7app.use(cors({ origin: ['https://myapp.com'] })); // whitelist only
8
9app.use(rateLimit({
10  windowMs: 15 * 60 * 1000, // 15 minutes
11  max: 100,                  // 100 requests per IP
12  message: 'Too many requests - slow down',
13}));
14
15// Centralised error handler - never expose stack traces in production
16app.use((err, req, res, next) => {
17  res.status(err.status || 500).json({
18    error: process.env.NODE_ENV === 'production' ? 'Internal Server Error' : err.message,
19  });
20});
21
22// Password hashing
23const hash  = await bcrypt.hash(plainPassword, 12);
24const valid = await bcrypt.compare(plainPassword, hash);
25

8. How do you implement a graceful shutdown in Node.js?

A graceful shutdown stops the server cleanly when it receives a termination signal (SIGTERM from Kubernetes, SIGINT from Ctrl+C) - finishing in-flight requests, draining queues, and closing DB connections before exiting. Without it, you risk dropped requests, data corruption, and connection pool exhaustion.

javascript
1let server;
2let isShuttingDown = false;
3
4// Reject new requests during shutdown
5app.use((req, res, next) => {
6  if (isShuttingDown) {
7    res.set('Connection', 'close');
8    return res.status(503).json({ error: 'Server shutting down' });
9  }
10  next();
11});
12
13server = app.listen(3000, () => console.log('Listening on 3000'));
14
15async function shutdown(signal) {
16  console.log(`${signal} received - graceful shutdown starting`);
17  isShuttingDown = true;
18
19  server.close(async () => {
20    try {
21      await db.pool.end();      // drain DB pool
22      await redis.quit();       // close Redis
23      await queue.close();      // drain message queue
24      console.log('Clean exit');
25      process.exit(0);
26    } catch (err) {
27      console.error(err);
28      process.exit(1);
29    }
30  });
31
32  // Force exit after 30s if drain takes too long
33  setTimeout(() => { console.error('Forced exit'); process.exit(1); }, 30_000);
34}
35
36process.on('SIGTERM', () => shutdown('SIGTERM')); // Kubernetes pod termination
37process.on('SIGINT',  () => shutdown('SIGINT'));  // Ctrl+C
38

9. What are the best practices for building a REST API with Node.js?

A production-grade REST API needs to be consistent, secure, observable, and resilient. The difference between a weekend project and a production API is almost entirely in these details.

AreaBest Practice
VersioningPrefix all routes with /api/v1/ so breaking changes ship as /api/v2/ without affecting existing clients
HTTP methodsUse correct semantics: GET (read), POST (create), PUT/PATCH (update), DELETE (delete)
Status codes200 OK, 201 Created, 400 Bad Request, 401 Unauthorised, 403 Forbidden, 404 Not Found, 429 Rate Limited, 500 Server Error
ValidationValidate all incoming data at the route level with zod or joi before it touches business logic
PaginationNever return unlimited results. Cursor-based pagination for large datasets; offset for small ones.
Error shapeReturn consistent error objects: { error: { code, message } }. Never expose stack traces in production.
LoggingStructured JSON logs with requestId, userId, method, url, status, and latency on every request
Health checkExpose /health that checks DB and cache connectivity for Kubernetes readiness/liveness probes
javascript
1const { z } = require('zod');
2
3const createUserSchema = z.object({
4  name:  z.string().min(2).max(100),
5  email: z.string().email(),
6});
7
8// Versioned, validated, paginated route with consistent response shapes
9app.post('/api/v1/users', authenticate, async (req, res, next) => {
10  const parsed = createUserSchema.safeParse(req.body);
11  if (!parsed.success)
12    return res.status(400).json({ error: { code: 'VALIDATION_ERROR', details: parsed.error.errors } });
13
14  try {
15    const user = await userService.create(parsed.data);
16    res.status(201).json({ data: user });  // 201 for creation
17  } catch (err) { next(err); }
18});
19
20// Health check - checked by Kubernetes every 10s
21app.get('/health', async (req, res) => {
22  const db    = await pool.query('SELECT 1').then(() => true).catch(() => false);
23  const cache = await redis.ping().then(() => true).catch(() => false);
24  const ok    = db && cache;
25  res.status(ok ? 200 : 503).json({ status: ok ? 'ok' : 'degraded', checks: { db, cache } });
26});
27

Found this helpful?

Share it with your network

Related Articles

Frontend

React JS

Prepare for your React interview with the most asked questions for freshers and experienced developers. Covers hooks, lifecycle, performance optimization, and real-world scenarios.

Fullstack

Next Js

Explore the most important Next.js interview questions including SSR, SSG, ISR, routing, performance optimization, and real-world implementation examples.