Node.js is not a programming language β it is a JavaScript runtime environment built on top of Google’s V8 engine. Understanding what Node.js actually is, how V8 compiles and executes JavaScript, and what the Node.js runtime adds on top of the engine is essential context for every concept that follows in this course. Developers who skip this foundation often struggle to explain why Node.js is fast for I/O tasks, slow for CPU tasks, and why you should never block the event loop. This lesson gives you that foundation.
What is V8?
| Component | Role | Language |
|---|---|---|
| V8 Engine | Compiles and executes JavaScript | C++ |
| Ignition | V8’s interpreter β converts JS to bytecode | C++ |
| TurboFan | V8’s optimising JIT compiler β hot code to machine code | C++ |
| Orinoco | V8’s garbage collector β reclaims unused memory | C++ |
| libuv | Cross-platform async I/O library β powers the event loop | C |
| Node.js Bindings | Bridge between JavaScript and OS system calls | C++ / JS |
| Node.js Standard Library | Built-in modules: fs, http, path, events, stream⦠| JavaScript |
Node.js vs Browser JavaScript
| Feature | Node.js | Browser |
|---|---|---|
| JavaScript Engine | V8 | V8 (Chrome), SpiderMonkey (Firefox), JavaScriptCore (Safari) |
| Global Object | global / globalThis |
window / globalThis |
| DOM APIs | Not available | Available β document, window, navigator |
| File System Access | Full access via fs module |
None (sandboxed) |
| Network | Full TCP/UDP, raw HTTP | fetch, WebSocket only |
| Module System | CommonJS + ES Modules | ES Modules (script type=”module”) |
| Process Control | process.exit(), process.env |
Not available |
| Threading | Single thread + Worker Threads + libuv thread pool | Single thread + Web Workers |
How V8 Executes JavaScript β The Pipeline
| Step | Stage | What Happens |
|---|---|---|
| 1 | Parsing | Source code β Abstract Syntax Tree (AST) |
| 2 | Compilation (Ignition) | AST β Bytecode β fast, unoptimised |
| 3 | Execution | Ignition interprets bytecode β collects profiling data |
| 4 | Optimisation (TurboFan) | Hot functions β native machine code β very fast |
| 5 | Deoptimisation | If assumptions break (e.g. type changes) β back to bytecode |
| 6 | Garbage Collection | Orinoco reclaims memory from objects no longer reachable |
The Node.js Runtime Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your Node.js Application β
β (JavaScript β your app code + npm packages) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Node.js Standard Library (JavaScript) β
β fs http path events stream crypto os util ... β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Node.js Bindings (C++ Addons / N-API) β
β Bridge: JavaScript β Native OS functionality β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ€
β V8 Engine (C++) β libuv (C) β
β βββββββββββββββββββββββ β ββββββββββββββββββββββββββββ β
β β Ignition (bytecode) β β β Event Loop β β
β β TurboFan (JIT) β β β Thread Pool (4 threads) β β
β β Orinoco (GC) β β β Async I/O (epoll/kqueue) β β
β βββββββββββββββββββββββ β ββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββ
β
Operating System (Linux / macOS / Windows)
Key Node.js Global Objects
// ββ process β information about the running Node.js process βββββββββββββββ
console.log(process.version); // 'v20.11.0'
console.log(process.platform); // 'linux', 'darwin', 'win32'
console.log(process.arch); // 'x64', 'arm64'
console.log(process.pid); // process ID β e.g. 12345
console.log(process.uptime()); // seconds since process started
console.log(process.memoryUsage()); // { rss, heapTotal, heapUsed, external }
console.log(process.cwd()); // current working directory
console.log(process.argv); // ['node', 'app.js', '--port', '3000']
console.log(process.env.NODE_ENV); // environment variables
// Exit codes
process.exit(0); // success
process.exit(1); // failure
// Listen for signals
process.on('SIGTERM', () => {
console.log('Shutting down gracefully...');
server.close(() => process.exit(0));
});
// ββ __dirname and __filename (CommonJS only) ββββββββββββββββββββββββββββββ
console.log(__dirname); // absolute path to current file's directory
console.log(__filename); // absolute path to current file
// In ES Modules (no __dirname available β use this instead):
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// ββ Buffer β raw binary data ββββββββββββββββββββββββββββββββββββββββββββββ
const buf = Buffer.from('Hello, Node.js');
console.log(buf); //
console.log(buf.toString('utf8')); // 'Hello, Node.js'
console.log(buf.toString('base64')); // 'SGVsbG8sIE5vZGUuanM='
console.log(buf.length); // 14 (bytes, not characters)
// ββ setTimeout / setInterval / setImmediate βββββββββββββββββββββββββββββββ
const timer = setTimeout(() => console.log('1 second later'), 1000);
clearTimeout(timer); // cancel it
setInterval(() => console.log('every 500ms'), 500);
// setImmediate fires after I/O callbacks, before setTimeout(fn, 0)
setImmediate(() => console.log('after I/O, before timers'));
// queueMicrotask β highest priority async callback
queueMicrotask(() => console.log('microtask β runs first'));
// Execution order:
// 1. queueMicrotask
// 2. setImmediate (in I/O context)
// 3. setTimeout(fn, 0)
How It Works
Step 1 β V8 Parses Your JavaScript into an AST
When Node.js runs a file, V8 reads the source code as a string and parses it into an Abstract Syntax Tree β a data structure representing the grammatical structure of your code. Syntax errors are caught at this stage before a single line executes. The AST is then handed to the Ignition interpreter.
Step 2 β Ignition Compiles AST to Bytecode and Executes It
Ignition converts the AST to compact bytecode and begins executing it immediately. This is faster to start than waiting for full compilation. As Ignition executes, it collects profiling data β which functions are called frequently, what types their arguments have, which branches are taken. This data is used by TurboFan.
Step 3 β TurboFan Optimises Hot Functions
Functions called many times (“hot”) are sent to TurboFan, which compiles them to optimised native machine code based on the type assumptions gathered by Ignition. The next time the function is called, native code runs directly β no interpretation overhead. If a type assumption breaks (the function is called with a different argument type), TurboFan deoptimises and Ignition takes over again.
Step 4 β libuv Handles All Async I/O
When your JavaScript calls an async operation β reading a file, making a network request, querying a database β Node.js does not block and wait. Instead, it delegates the I/O operation to libuv, which uses the operating system’s most efficient async I/O mechanism (epoll on Linux, kqueue on macOS, IOCP on Windows). Node.js registers a callback and moves on. When the OS signals completion, libuv places the callback in the event loop queue.
Step 5 β The process Object Is Your Runtime Interface
The process object is the bridge between your JavaScript code and the Node.js process itself. It exposes environment variables, command-line arguments, the working directory, memory usage, and process lifecycle events. You will use process.env constantly for configuration and process.on('SIGTERM', ...) for graceful shutdown in production.
Real-World Example: Application Startup Diagnostics
// startup-diagnostics.js
// Run at application start to log runtime environment information
function logStartupInfo() {
const mem = process.memoryUsage();
const toMB = bytes => (bytes / 1024 / 1024).toFixed(2);
console.log('βββββββββββββββββββββββββββββββββββββββ');
console.log(' Application Starting');
console.log('βββββββββββββββββββββββββββββββββββββββ');
console.log(` Node.js : ${process.version}`);
console.log(` Platform : ${process.platform} (${process.arch})`);
console.log(` PID : ${process.pid}`);
console.log(` ENV : ${process.env.NODE_ENV ?? 'development'}`);
console.log(` CWD : ${process.cwd()}`);
console.log(` Heap Used : ${toMB(mem.heapUsed)} MB`);
console.log(` Heap Total: ${toMB(mem.heapTotal)} MB`);
console.log(` RSS : ${toMB(mem.rss)} MB`);
console.log('βββββββββββββββββββββββββββββββββββββββ');
}
// Graceful shutdown handler
function setupGracefulShutdown(server) {
const shutdown = async (signal) => {
console.log(`
[${signal}] Shutdown signal received`);
server.close(async () => {
console.log('HTTP server closed');
// Close database connection, flush logs, etc.
process.exit(0);
});
// Force exit if graceful shutdown takes too long
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 10_000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
shutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
shutdown('unhandledRejection');
});
}
module.exports = { logStartupInfo, setupGracefulShutdown };
Common Mistakes
Mistake 1 β Using synchronous fs methods in a server request handler
β Wrong β blocks the event loop for every request:
app.get('/config', (req, res) => {
const data = fs.readFileSync('./config.json'); // BLOCKS β all other requests wait
res.json(JSON.parse(data));
});
β Correct β use the async version and keep the event loop free:
app.get('/config', async (req, res) => {
const data = await fs.promises.readFile('./config.json');
res.json(JSON.parse(data));
});
Mistake 2 β Mutating process.env values
β Wrong β process.env values are strings, and mutations affect the whole process:
process.env.PORT = 3000; // stored as string '3000', not number
process.env.DEBUG = true; // stored as string 'true', not boolean
if (process.env.DEBUG) { } // always truthy β even 'false' is truthy!
β Correct β parse and validate env vars at startup in one place:
// config/env.js β loaded once at startup
const PORT = parseInt(process.env.PORT, 10) || 3000;
const DEBUG = process.env.DEBUG === 'true';
module.exports = { PORT, DEBUG };
Mistake 3 β Treating Node.js like a browser environment
β Wrong β using browser globals that do not exist in Node.js:
document.getElementById('app'); // ReferenceError: document is not defined
window.localStorage.getItem('token'); // ReferenceError: window is not defined
fetch('https://api.example.com'); // fetch is available in Node 18+ but not earlier
β Correct β use Node.js equivalents:
// Node.js has no DOM β use http/https module or node-fetch for HTTP requests
const https = require('https');
// OR in Node 18+:
const response = await fetch('https://api.example.com'); // built-in
Quick Reference
| Global | Type | Purpose |
|---|---|---|
process |
Object | Runtime info, env vars, argv, signals, exit |
__dirname |
String (CJS) | Absolute path to the current file’s directory |
__filename |
String (CJS) | Absolute path to the current file |
Buffer |
Class | Binary data β encoding, decoding, slicing |
global |
Object | Global namespace (like window in browsers) |
setTimeout |
Function | Schedule callback after delay (macrotask) |
setImmediate |
Function | Schedule callback after I/O (Node.js-specific) |
queueMicrotask |
Function | Schedule microtask β highest async priority |