#What Node.js is: V8, libuv and the bindings
AddedNode.js is not a language and not a framework. It is a runtime: a C++ program that embeds a JavaScript engine and gives it access to the outside world. On its own, JavaScript can add numbers, build objects and call functions, but it has no idea what a file, a socket or a clock is. Node supplies those.
It is built from four layers:
- V8 — Google's JavaScript engine (the one in Chrome). It parses and compiles your code, runs it on the call stack, owns the heap and does garbage collection. V8 knows nothing about files, networks or timers.
- libuv — a C library that provides the event loop, cross-platform asynchronous I/O (epoll on Linux, kqueue on macOS, IOCP on Windows), timers, and a small thread pool for work the OS can't do asynchronously.
- The C++ bindings — Node's own glue code. They expose libuv, OpenSSL (crypto), zlib (compression), llhttp (HTTP parsing) and c-ares (DNS) to JavaScript as native functions.
- Node's JavaScript library — the
node:fs,node:http,node:events… modules yourequire. They are written in JavaScript and call down into the bindings.
A mental model: V8 is a chef who can only cook; libuv is the kitchen manager with a pager, a phone and a few helpers. The chef never waits by the oven or at the door. They hand the waiting to the manager, keep cooking, and the manager hands them a ticket when something is ready. The bindings are the intercom between them.
You can see the parts and their versions in any Node:
const v = process.versions;
console.log({ node: v.node, v8: v.v8, uv: v.uv, openssl: v.openssl, zlib: v.zlib, llhttp: v.llhttp, ares: v.ares });
// {
// node: '26.7.0',
// v8: '14.6.202.34-node.28',
// uv: '1.52.1',
// openssl: '3.6.3',
// zlib: '1.2.12',
// llhttp: '9.4.3',
// ares: '1.34.8'
// }Each line is a separate project that Node bundles: the engine (v8), the event loop (uv is libuv), and the native libraries the bindings wrap. When Node's release notes say "libuv updated to 1.45", that is where event-loop behaviour can change — chapter 2's phases topic has a real example.
The clearest way to see the layers working together is to look at the stack inside a callback:
const fs = require('node:fs');
fs.readFile(__filename, () => {
console.log(new Error('inside the readFile callback').stack);
});
setTimeout(() => console.log(new Error('inside a timer').stack), 5);
setImmediate(() => console.log(new Error('inside setImmediate').stack));
// Error: inside setImmediate
// at Immediate._onImmediate (…/stack.cjs:7:32)
// at process.processImmediate (node:internal/timers:574:21)
// Error: inside the readFile callback
// at …/stack.cjs:4:15
// at FSReqCallback.readFileAfterClose [as oncomplete] (node:internal/fs/read/context:96:3)
// Error: inside a timer
// at Timeout._onTimeout (…/stack.cjs:6:30)
// at listOnTimeout (node:internal/timers:685:17)
// at process.processTimers (node:internal/timers:618:7)- The script runs top to bottom on the main thread.
fs.readFile(Node's JS library) creates a native request object,FSReqCallback, through the bindings and hands the actual file reading to libuv.setTimeoutandsetImmediatejust register callbacks. All three calls return immediately and the script ends. - Notice what is missing from every trace: there is no
stack.cjstop-level frame and noreadFileframe. By the time each callback runs, the script that scheduled it has long finished and its stack frames are gone. - The bottom frame of each trace is where the loop came back into JavaScript. For the file read, libuv finished the work, the C++ binding called the request's
oncompletemethod (readFileAfterClose), and that called your function. - For the timer and the immediate, the loop called Node's internal
processTimersandprocessImmediatefunctions, which walk Node's own lists and call your callbacks one by one. - This is the whole architecture in one picture: your JavaScript only ever runs because the loop (C/C++) calls into V8, either to run the main script or to deliver a callback. The order of these three is not a coincidence — the setTimeout vs setImmediate topic explains it.
What runs on the main thread: your code, Node's JavaScript library, every callback, every promise reaction, and the libuv loop itself (including the "wait for I/O" system call). What runs elsewhere: libuv's thread pool (file system, crypto.pbkdf2, zlib, dns.lookup), V8's helper threads (parts of garbage collection and compilation), and the operating system kernel, which does the actual network I/O. Chapter 2 starts with exactly which work goes where.