Move Introduction to Node.js Internals
Open Introduction to Node.js Internals
Introduction to Node.js Internals
Node.js is not just JavaScript on the server - it's a carefully architected system that combines JavaScript with powerful C++ internals to handle concurrent operations efficiently on a single thread.
What Makes Node Special?
Most server platforms create a new thread for each incoming request. With thousands of concurrent users, this means thousands of threads, each consuming memory and CPU resources for context switching.
Node takes a radically different approach:
Traditional Server (Thread per Request):
Request 1 → Thread 1 → [waiting for DB] → Response
Request 2 → Thread 2 → [waiting for file] → Response
Request 3 → Thread 3 → [waiting for API] → Response
Node.js (Single Thread + Event Loop):
Request 1 → Event Loop → [queue DB callback] → Response
Request 2 → Event Loop → [queue file callback] → Response
Request 3 → Event Loop → [queue API callback] → Response
The Two-Language Architecture
Node.js is fundamentally **two langua
Introduction to Node.js Internals
480 words
Move Node.js Architecture
Open Node.js Architecture
Node.js Architecture
Understanding Node's architecture is crucial for writing efficient server-side JavaScript. Let's examine how JavaScript, V8, libuv, and the operating system work together.
The Component Stack
┌─────────────────────────────────────────────────┐
│ Your JavaScript Code │
├─────────────────────────────────────────────────┤
│ Node.js APIs │
│ (fs, http, net, crypto, etc.) │
├─────────────────────────────────────────────────┤
│ Node.js Bindings │
│ (C++ bridging layer) │
├─────────────────────────────────────────────────┤
│ V8 Engine │ libuv │
│ (JS execution) │ (Async I/O, loop) │
├─────────────────────────────────────────────────┤
│ Operating System │
│ (Linux, macOS, Windows system calls) │
└────────────────────────────────────────────
Node.js Architecture
547 words
Move The Event Loop: Overview
Open The Event Loop: Overview
The Event Loop: Overview
The event loop is the heart of Node.js. It's the mechanism that allows Node to perform non-blocking I/O operations despite JavaScript being single-threaded.
Why Do We Need It?
JavaScript can only execute one piece of code at a time (single-threaded). But servers need to:
- Handle thousands of concurrent connections
- Read/write files without blocking
- Make network requests without waiting
- Process timers and intervals
The event loop solves this by offloading operations to the system kernel (when possible) or a thread pool, then queuing callbacks when those operations complete.
The Event Loop Diagram
┌───────────────────────────┐
┌─>│ timers │ setTimeout, setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ System operation callbacks
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ Internal use only
The Event Loop: Overview
655 words
Move Timers and Poll Phase
Open Timers and Poll Phase
Timers and Poll Phase
Two of the most important event loop phases are timers and poll. Understanding how they work together is key to writing predictable async code.
The Timers Phase
How Timers Work
When you call setTimeout() or setInterval(), Node doesn't create a new timer at the OS level for each one. Instead, it:
- Records the callback and threshold time
- Maintains a sorted list of pending timers
- Calculates when to check for due timers in poll phase
// Timer threshold = minimum wait time, not exact execution time
setTimeout(() => {
console.log('This runs AFTER at least 100ms');
}, 100);
Timer Execution Order
Timers with the same delay fire in order of registration:
setTimeout(() => console.log('A'), 100);
setTimeout(() => console.log('B'), 100);
setTimeout(() => console.log('C'), 100);
// Output: A, B, C (always in this order)
Timer Coalescing
Node coalesces timers for efficiency:
Timers and Poll Phase
770 words
Move Check Phase and Close Callbacks
Open Check Phase and Close Callbacks
Check Phase and Close Callbacks
The check phase and close callbacks phase complete the event loop cycle. While less discussed than timers and poll, they serve important roles.
The Check Phase
Purpose
The check phase exists specifically for setImmediate(). It runs callbacks immediately after the poll phase completes.
setImmediate(() => {
console.log('This runs in check phase');
});
Why setImmediate Exists
You might wonder: why not just use setTimeout(fn, 0)?
// These are NOT equivalent:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
The difference:
setTimeout(fn, 0) → Runs in timers phase (next iteration)
setImmediate(fn) → Runs in check phase (same iteration, after poll)
The Key Guarantee
Inside an I/O callback, setImmediate always fires first:
const fs = require('fs');
fs.readFile(__filename, () => {
// Currently in
Check Phase and Close Callbacks
770 words
Move process.nextTick and Microtasks
Open process.nextTick and Microtasks
process.nextTick and Microtasks
These two mechanisms have the highest priority in Node's execution order. Understanding them is crucial for predictable async behavior.
process.nextTick()
Not Part of the Event Loop
Despite its name, process.nextTick() is NOT part of the event loop. It runs between phases, before any phase begins.
setImmediate(() => console.log('1. Check phase'));
setTimeout(() => console.log('2. Timer phase'), 0);
process.nextTick(() => console.log('3. nextTick'));
console.log('4. Sync');
// Output: 4, 3, 2, 1 (or 4, 3, 1, 2)
// nextTick ALWAYS runs before any event loop phase
The nextTick Queue
All nextTick callbacks are processed before continuing:
process.nextTick(() => {
console.log('tick 1');
process.nextTick(() => console.log('tick 2'));
});
setTimeout(() => console.log('timeout'), 0);
// Output: tick 1, tick 2, timeout
// All nextTicks drain before timer phase
Warning: I/
process.nextTick and Microtasks
769 words
Move Streams: Introduction
Open Streams: Introduction
Streams: Introduction
Streams are one of Node's most powerful features. They let you process data piece by piece, without loading everything into memory at once.
The Problem Streams Solve
Without streams:
const fs = require('fs');
// This loads ENTIRE file into memory
const data = fs.readFileSync('huge-file.csv'); // 2GB = 2GB RAM
processData(data);
With streams:
const fs = require('fs');
// This processes file in small chunks
const stream = fs.createReadStream('huge-file.csv');
stream.on('data', (chunk) => {
processChunk(chunk); // 64KB at a time
});
Four Types of Streams
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Readable │─────>│ Transform │─────>│ Writable │
│ Stream │ │ Stream │ │ Stream │
└─────────────┘ └──────────────┘ └─────────────┘
│
(can also be)
│
Streams: Introduction
718 words
Move Readable Streams
Open Readable Streams
Readable Streams
Readable streams are sources of data. Understanding their two modes and how to consume them is essential for efficient data processing.
Creating Readable Streams
From Files
const fs = require('fs');
const readable = fs.createReadStream('data.txt', {
encoding: 'utf8', // Return strings instead of Buffers
highWaterMark: 16384, // Chunk size (16KB)
});
From HTTP Requests
const http = require('http');
http.get('http://example.com/data', (response) => {
// response is a readable stream
response.on('data', (chunk) => {
console.log(chunk.toString());
});
});
Custom Readable
const { Readable } = require('stream');
const readable = new Readable({
read(size) {
// Push data when read() is called
this.push('Hello ');
this.push('World!');
this.push(null); // Signal end of stream
}
});
Two Reading Modes
1. Flowing Mode
Data is r
Readable Streams
737 words
Move Writable and Transform Streams
Open Writable and Transform Streams
Writable and Transform Streams
Writable streams consume data, and transform streams modify data as it passes through. Together with readable streams, they form the complete streaming picture.
Writable Streams
Creating Writable Streams
const fs = require('fs');
const writable = fs.createWriteStream('output.txt', {
encoding: 'utf8',
highWaterMark: 16384, // 16KB buffer
flags: 'w' // 'w' write, 'a' append
});
Writing Data
// write() returns boolean
const canContinue = writable.write('Hello World\n');
// If false, buffer is full - should pause
if (!canContinue) {
console.log('Buffer full, waiting for drain...');
}
// Signal end of writing
writable.end('Final data\n');
Important Events
const writable = fs.createWriteStream('output.txt');
// Buffer drained, ready for more data
writable.on('drain', () => {
console.log('Ready for more data');
});
// All data has been flushe
Writable and Transform Streams
796 words
Move HTTP Server Fundamentals
Open HTTP Server Fundamentals
HTTP Server Fundamentals
Node's HTTP module lets you build web servers from scratch. Understanding the underlying mechanics helps you appreciate what frameworks like Express abstract away.
Creating a Basic Server
const http = require('http');
const server = http.createServer((request, response) => {
// This callback runs for EVERY incoming request
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end('Hello World');
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
The Request Object
The request (http.IncomingMessage) is a Readable Stream:
http.createServer((req, res) => {
// Request metadata
console.log(req.method); // 'GET', 'POST', etc.
console.log(req.url); // '/path?query=string'
console.log(req.headers); // { 'content-type': '...', ... }
console.log(req.httpVersion); // '1.1'
// For POST/PUT - read body as stream
l
HTTP Server Fundamentals
859 words
Move Request-Response Pattern Under the Hood
Open Request-Response Pattern Under the Hood
Request-Response Pattern Under the Hood
Understanding what happens between a client request and server response reveals how Node.js leverages the event loop for scalability.
The Complete Flow
Client Node.js Server Operating System
│ │ │
│──────TCP Connect──────────>│ │
│ │<────notify connection─────│
│ │ (libuv poll phase) │
│ │ │
│────HTTP Request───────────>│ │
│ │<────data available────────│
│ │ (poll phase callback) │
│ │ │
│ │ [Your code runs] │
│ │ │
│<───HTTP Response───────────│
Request-Response Pattern Under the Hood
898 words
Move Practical Patterns and Best Practices
Open Practical Patterns and Best Practices
Practical Patterns and Best Practices
Let's consolidate everything with real-world patterns for production Node.js applications.
Error Handling Patterns
Never Throw in Async Code
// BAD: Unhandled rejection
async function fetchData() {
const response = await fetch('https://api.example.com');
if (!response.ok) {
throw new Error('Failed to fetch'); // Becomes unhandled rejection
}
return response.json();
}
// GOOD: Handle at call site
async function handler(req, res) {
try {
const data = await fetchData();
res.json(data);
} catch (err) {
console.error('Fetch error:', err);
res.status(500).json({ error: 'Internal error' });
}
}
Global Error Handlers
// Catch unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
// Log to monitoring service
// Optionally: process.exit(1);
});
// Catch uncaught exceptions
Practical Patterns and Best Practices
983 words