Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Node.js

Overview

Node.js is a JavaScript runtime built on Chrome’s V8 engine. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient for building scalable network applications. Understanding Node.js internals is essential for backend interviews.

Architecture

graph TB
    subgraph "Node.js Process"
        APP[Your JavaScript Code]
        V8[V8 Engine]
        LIBUV[libuv]
        BINDINGS[C++ Bindings]
        NATIVE[Native Modules]
    end
    APP --> V8
    V8 --> BINDINGS
    BINDINGS --> LIBUV
    BINDINGS --> NATIVE
    LIBUV --> THREADPOOL[Thread Pool<br/>Default: 4 threads]
    LIBUV --> EPOLL[OS Async I/O<br/>epoll/kqueue/IOCP]
ComponentRole
V8JavaScript execution, JIT compilation
libuvEvent loop, async I/O, thread pool
C++ BindingsBridge between JS and native code
Native ModulesFile system, crypto, network (C/C++)

Event Loop

The event loop is the heart of Node.js. It’s a single-threaded loop that processes callbacks and I/O events.

Event Loop Phases

graph LR
    subgraph "Event Loop (libuv)"
        T[Timers] --> P[Pending Callbacks]
        P --> I[Idle/Prepare]
        I --> C[Check]
        C --> CC[Close Callbacks]
        CC --> T
    end
    MT[Microtasks<br/>process.nextTick / Promises] --> T
    MT --> P
    MT --> I
    MT --> C
    MT --> CC
   ┌───────────────────────────┐
┌─>│           timers          │  setTimeout, setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  system callbacks (TCP errors)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  internal use only
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  I/O callbacks (fs, net)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  setImmediate callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │      close callbacks      │  socket.on('close')
│  └─────────────┬─────────────┘
│                │
└────────────────┘

Phase Details

PhaseWhat RunsExample
TimersExpired timer callbackssetTimeout(), setInterval()
Pending CallbacksDeferred system-level callbacksTCP errors, some I/O
Idle/PrepareInternal (libuv bookkeeping)
PollI/O events, incoming connectionsfs.readFile(), http requests
ChecksetImmediate() callbackssetImmediate()
CloseCleanup callbackssocket.on('close')

Microtasks vs Macrotasks

// Microtasks run BETWEEN phases, not in a specific phase
setImmediate(() => console.log('setImmediate'));    // check phase
setTimeout(() => console.log('setTimeout'), 0);     // timers phase
Promise.resolve().then(() => console.log('Promise')); // microtask
process.nextTick(() => console.log('nextTick'));     // microtask (priority)

// Execution order:
// 1. nextTick (highest priority microtask)
// 2. Promise (microtask)
// 3. setTimeout (timers phase) OR setImmediate (check phase) — depends on timing
TypeAPIPriorityWhen
Microtaskprocess.nextTick()HighestAfter current operation, before event loop continues
MicrotaskPromise.then()HighAfter current operation, before event loop continues
MacrotasksetTimeout()NormalTimers phase
MacrotasksetImmediate()NormalCheck phase
MacrotaskI/O callbacksNormalPoll phase

Streams

Streams are Node.js’s way of handling reading/writing data piece by piece, rather than loading everything into memory.

Stream Types

graph LR
    R[Readable] --> T[Transform] --> W[Writable]
    R --> W
TypeDescriptionExamples
ReadableSource of datafs.createReadStream(), http.IncomingMessage
WritableDestination for datafs.createWriteStream(), http.ServerResponse
DuplexBoth readable and writablenet.Socket, zlib streams
TransformModify data as it passes throughzlib.createGzip(), crypto.createCipheriv()

Backpressure

// ✅ Proper backpressure handling
const readable = fs.createReadStream('huge-file.txt');
const writable = fs.createWriteStream('output.txt');

readable.pipe(writable);  // Node handles backpressure automatically

// ❌ Without backpressure — memory explosion
readable.on('data', (chunk) => {
  writable.write(chunk);  // writable.write() returns false but we ignore it
});

Stream Modes

ModeBehaviorUse Case
FlowingData flows automaticallypipe(), on('data')
PausedMust manually read()Fine-grained control

Practical Example: File Processing Pipeline

const { createReadStream, createWriteStream } = require('fs');
const { createGzip } = require('zlib');
const { Transform } = require('stream');

const upperCase = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk.toString().toUpperCase());
  }
});

createReadStream('input.txt')
  .pipe(upperCase)
  .pipe(createGzip())
  .pipe(createWriteStream('output.txt.gz'))
  .on('finish', () => console.log('Done'));

Clusters

The cluster module allows creating child processes that share the same server port, enabling multi-core utilization.

How Clusters Work

graph TB
    M[Master Process] --> W1[Worker 1<br/>Port 3000]
    M --> W2[Worker 2<br/>Port 3000]
    M --> W3[Worker 3<br/>Port 3000]
    M --> W4[Worker 4<br/>Port 3000]
    C[Client] --> LB[Load Balancer<br/>Round-Robin]
    LB --> W1
    LB --> W2
    LB --> W3
    LB --> W4

Cluster Example

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} died, restarting...`);
    cluster.fork();  // Respawn
  });
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello from worker ' + process.pid);
  }).listen(3000);

  console.log(`Worker ${process.pid} started`);
}

Cluster vs Worker Threads

FeatureClusterWorker Threads
MemorySeparate V8 instancesShared memory (SharedArrayBuffer)
CommunicationIPC (serialization)postMessage + SharedArrayBuffer
Use CaseI/O-bound (HTTP servers)CPU-bound (data processing)
OverheadHigher (full process)Lower (lightweight thread)
Fault isolationProcess-levelThread-level

Worker Threads

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // Main thread
  const worker = new Worker(__filename, {
    workerData: { numbers: [1, 2, 3, 4, 5] }
  });

  worker.on('message', (result) => {
    console.log('Sum:', result);  // Sum: 15
  });
} else {
  // Worker thread
  const sum = workerData.numbers.reduce((a, b) => a + b, 0);
  parentPort.postMessage(sum);
}

When to Use What

flowchart TD
    A[Task] --> B{CPU-intensive?}
    B -->|Yes| C{Need shared memory?}
    C -->|Yes| D[Worker Threads]
    C -->|No| E[Either works]
    B -->|No| F[Single process<br/>with async I/O]
    F --> G{Need multi-core?}
    G -->|Yes| H[Cluster Module]
    G -->|No| F

Module System

CommonJS (CJS)

// math.js
const PI = 3.14159;
function add(a, b) { return a + b; }
module.exports = { PI, add };

// app.js
const { PI, add } = require('./math');

ES Modules (ESM)

// math.mjs
export const PI = 3.14159;
export function add(a, b) { return a + b; }

// app.mjs
import { PI, add } from './math.mjs';

CJS vs ESM

FeatureCommonJSES Modules
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
AnalysisRuntimeStatic (compile-time)
Tree-shaking❌ Not possible✅ Possible
Circular depsPartial objectLive bindings
Top-level await

Error Handling

The Error Hierarchy

graph TD
    E[Error] --> RE[ReferenceError]
    E --> TE[TypeError]
    E --> SE[SyntaxError]
    E --> RE2[RangeError]
    E --> NE[NetworkError]
    E --> AE[AggregateError]
    E --> CE[CustomError]

Error Handling Patterns

// 1. try/catch with async/await
async function fetchData() {
  try {
    const data = await fetch('https://api.example.com');
    return await data.json();
  } catch (error) {
    console.error('Fetch failed:', error.message);
    throw error;  // Re-throw or handle
  }
}

// 2. Callback error-first pattern (legacy)
fs.readFile('file.txt', (err, data) => {
  if (err) {
    console.error('Read failed:', err);
    return;
  }
  console.log(data);
});

// 3. Express error middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
});

// 4. Unhandled rejection handler
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // In production: log and exit gracefully
  process.exit(1);
});

npm and Package Management

package.json Essentials

{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "jest",
    "dev": "nodemon index.js"
  },
  "dependencies": {
    "express": "^4.18.0"
  },
  "devDependencies": {
    "jest": "^29.0.0"
  }
}

Semantic Versioning

SymbolMeaningExample
^4.18.0Compatible with 4.x.x>=4.18.0 <5.0.0
~4.18.0Compatible with 4.18.x>=4.18.0 <4.19.0
4.18.0Exact version4.18.0
*Any versionlatest

Interview Questions

Q: Explain the Node.js event loop phases.

A: The event loop has 6 phases executed in order: 1) Timers — runs setTimeout/setInterval callbacks, 2) Pending callbacks — deferred system callbacks, 3) Idle/prepare — internal, 4) Poll — I/O callbacks, 5) Check — setImmediate callbacks, 6) Close — cleanup. Microtasks (process.nextTick, Promises) run between every phase, not in a specific phase. nextTick has higher priority than Promises.

Q: When would you use clusters vs worker threads?

A: Clusters create separate processes with independent V8 instances, best for I/O-bound work like HTTP servers. Worker threads are lightweight threads that can share memory via SharedArrayBuffer, best for CPU-intensive tasks. Clusters have higher overhead but better fault isolation. Worker threads have lower overhead and can share memory but share the same process.

Q: How do Node.js streams handle backpressure?

A: When a writable stream can’t keep up with a readable stream, write() returns false. The readable stream should pause until the writable stream emits ‘drain’. pipe() handles this automatically. Without backpressure handling, data buffers in memory, potentially causing out-of-memory errors.

Q: Explain the difference between process.nextTick() and setImmediate().

A: process.nextTick() runs after the current operation completes, before the event loop continues to the next phase — it’s a microtask with highest priority. setImmediate() runs in the check phase, after poll phase I/O callbacks. nextTick can starve I/O if called recursively; setImmediate is safer for yielding to the event loop.

Q: How does Node.js handle the “single-threaded” misconception?

A: Node.js’s JavaScript execution is single-threaded (one V8 instance, one event loop). However, libuv uses a thread pool (default 4 threads) for blocking operations like file system access, DNS lookup, and crypto. Network I/O uses OS async mechanisms (epoll/kqueue) without threads. So Node.js is single-threaded for JS execution but multi-threaded under the hood.

References