Introduction to Node.js
Node.js lets you run JavaScript outside the browser: on your laptop, on a server, in a build tool or a command-line app. This chapter explains what Node actually is (and isn't), how it can serve thousands of users with a single thread, how it differs from the browser, and how its versions work.
You will learn
- What a JavaScript runtime is: V8 + libuv + the core modules.
- Why Node uses a single thread with non-blocking I/O, and why that scales.
- How Node differs from the browser: globals, APIs, modules and who controls the version.
- What V8 does with your code, and how much JavaScript you need to know.
- Node's release lines (Current vs LTS), version managers, and checking versions in code.
What is Node.js?
Node.js is a JavaScript runtime: a program that runs JavaScript files and gives them extra powers the language itself doesn't have, such as reading files, opening network connections and starting other programs. It's open source, cross-platform (Linux, macOS, Windows) and was created by Ryan Dahl in 2009.
Node is made of three main layers:
- V8, Google's JavaScript engine (the same one inside Chrome). It parses and runs your JavaScript, very fast.
- libuv, a C library that talks to the operating system: files, sockets, timers, child processes. It provides the event loop and a small thread pool.
- Core modules written in JavaScript and C++:
node:fs,node:http,node:path,node:events… They are the bridge between your code and libuv.
🧠 Analogy: engine and car. V8 is an engine. On its own it can't take you anywhere: it has no wheels, doors or dashboard. Chrome puts that engine in one car (with a DOM, tabs and a window), and Node puts the same engine in a different car (with a file system, network sockets and processes). Same engine, same JavaScript, very different vehicles.
Your first Node program
Save this as hello.js and run it with node hello.js:
There's no HTML page and no browser: console.log writes straight to your terminal (to standard output).
The classic example: a web server
The first example on nodejs.org is a web server in a few lines. Here's a version that starts a server, sends itself one request with fetch, prints the answer and shuts down, so it runs to completion:
In a real app you'd use a fixed port, like server.listen(3000), and the server would keep running until you stop it with Ctrl+C. Chapter 11 builds servers properly.
One thread, non-blocking I/O
A Node program runs your JavaScript on one main thread. Yet a single Node process can serve thousands of connections at once. How?
Most of a server's time is spent waiting: for the disk, a database or the network. In Node, I/O operations are non-blocking: you start them, give a callback (or get a promise), and Node keeps running other code. When the operating system says "the data is ready", Node queues your callback, and the event loop runs it as soon as the thread is free.
🧠 Analogy: one waiter, many tables. A restaurant with one waiter could work in two ways. A blocking waiter takes an order, walks to the kitchen and stands there until the dish is ready, ignoring every other table. A non-blocking waiter takes the order, pins it in the kitchen, and goes on to the next table; when the bell rings, the waiter carries the finished dish out. Node is the second waiter: the kitchen (the operating system and libuv) does the slow work, and the single waiter (your JavaScript thread) never stands still.
☕ Java: a classic Java servlet server uses one thread per request; a request waiting on the database blocks its thread. Node instead uses one thread plus asynchronous I/O, like Java's NIO/Netty or virtual threads in spirit. You don't share memory between request handlers on different threads, so there are no data races or locks, but a slow CPU computation blocks everyone (Chapter 13).
⚠️ Pitfall: non-blocking is about I/O, not CPU. A loop that computes for two seconds blocks the only thread: no other request, timer or callback runs meanwhile. Node is great for I/O-heavy work (APIs, proxies, real-time apps, tools) and needs extra care (worker threads) for heavy computation.
Node vs the browser
Both run JavaScript with V8-class engines and both use an event loop, so the language is identical. The environment is different:
| Browser | Node.js | |
|---|---|---|
| Global object | window (and globalThis) | globalThis (no window) |
| Page APIs | document, DOM, localStorage, cookies | none of these |
| System APIs | sandboxed: no file system | node:fs, node:net, node:child_process, process |
| Shared web APIs | fetch, URL, setTimeout, AbortController, TextEncoder, structuredClone, WebSocket, crypto.randomUUID… | |
| Modules | ES modules (<script type="module">) | ES modules and CommonJS (require) |
| JavaScript version | whatever your users' browsers support | you choose the Node version, so you know exactly |
| Security model | the page is untrusted, heavily sandboxed | your program has your user's full permissions |
🌐 Browser habit to unlearn: in the browser you never control your users' browser versions, so you transpile and polyfill. In Node, the version is a deployment decision. If your server runs Node 26, you can use every feature Node 26 supports — no Babel needed.
How much JavaScript do you need?
Node is JavaScript, so the more you know the language, the easier Node becomes. Before going deep, be comfortable with these topics (the links go to the JavaScript Study Book):
- Variables, types, functions, arrow functions,
thisand closures (Chapters 2–4). - Objects, arrays, classes, destructuring and spread (Chapters 5, 8–11).
- Strict mode and modules (Chapters 1 and 14).
- Most importantly, asynchronous JavaScript: callbacks, promises,
async/awaitand the event loop (Chapter 12). Node is asynchronous all the way down; Part II of this book revisits these ideas from Node's point of view.
The V8 engine
V8 is written in C++ and turns JavaScript into machine code while the program runs (just-in-time compilation). It starts by interpreting your code quickly (the Ignition interpreter), watches which functions run often ("hot" code) and recompiles those with an optimising compiler (TurboFan, with mid-tier compilers in between). It also manages memory with a garbage collector.
🧠 Analogy: an interpreter who becomes a translator. On day one, a conference interpreter translates each sentence as it comes. After hearing the same phrases a hundred times, they write a polished translation of those phrases and just read it out. That's V8: it interprets first, then optimises the code you run most. It's also why micro-benchmarks in JavaScript are tricky: the first runs are slower than later ones.
You can see which engine and library versions your Node uses:
node -p ("print") evaluates an expression and prints the result; you'll see it often in this book. Other engines exist (SpiderMonkey in Firefox, JavaScriptCore in Safari and Bun), but Node always uses V8.
Node versions and release lines
A new major version of Node is released every six months (in April and October). The release schedule works like this:
- Current: the newest major version, for six months. Good for trying new features.
- LTS (Long-Term Support): in October, the April release is promoted to LTS and gets a codename. It receives fixes for about 30 months. Production servers should run an LTS version.
- End-of-life: no more security fixes. Upgrade before you get here.
So Node 24 (codename Krypton) and Node 26 (codename Lithium) are both LTS lines in late 2026. Your code can find out what it's running on:
Version managers
Different projects may need different Node versions. A version manager installs several versions side by side and switches between them per project:
- nvm (macOS/Linux) and nvm-windows:
nvm install 26,nvm use 24. - fnm (fast, cross-platform) and Volta (pins versions in
package.json). - Corepack-style tooling and the
"engines"field inpackage.jsondocument the version a project expects.
Most version managers read a .nvmrc or .node-version file in the project folder, containing just a version like 26, so everyone on the team uses the same one.
💡 Tip: avoid installing Node with sudo or system package managers that lag behind. A version manager installs Node in your home folder, so global npm packages don't need administrator rights.
Modern JavaScript in Node
Node follows V8, so new language features arrive in Node a few months after they ship in Chrome. Everything from ES2015 (ES6) on is available in every supported Node version: classes, arrow functions, promises, async/await, optional chaining, private fields, top-level await in modules, and newer features like iterator helpers and Set methods. To check a specific feature, look at node.green or simply try it:
More examples
Facts about the machine
The node:os module describes the computer your program runs on. Your values will differ, so this example only prints the shape of the data:
One script, two environments
Code that should run both in the browser and in Node can check for the features it needs instead of guessing the environment:
Showing that the thread is single
Two timers are due after 10 ms, but a busy loop holds the thread for 50 ms first. The timers can only run once the thread is free:
Recap
- Node.js is a JavaScript runtime: V8 runs the code, libuv does the I/O and runs the event loop, and core modules (
node:fs,node:http…) give you system powers. - Your JavaScript runs on one thread. I/O is non-blocking, so one process can juggle thousands of connections; CPU-heavy work blocks everyone.
- Same language as the browser, different environment: no
window/document, butprocess, files, sockets and both module systems. You choose the Node version. - V8 compiles hot code just in time. Even-numbered majors become LTS; use LTS in production and a version manager (nvm, fnm, Volta) on your machine.
Exercises
Exercise 1 🟢 easy run it
Write greet.js that prints Hello, Node! and the current Node major version on a second line, like running on Node 26.
Show solution
Exercise 2 🟢 easy predict the output
What does each line print when run with node?
Show solution
undefined, object, true, string. Node has no window or document; globalThis is the standard name for the global object everywhere; process.version is a string like "v26.13.0".
Exercise 3 🟢 easy explain
Name the three main layers of Node.js and what each one does, in one sentence each.
Show solution
V8 parses, compiles and runs JavaScript and manages memory. libuv provides the event loop, asynchronous I/O (files, network, timers) and a thread pool. The core modules (node:fs, node:http, …) are the JavaScript API that connects your code to libuv and the operating system.
Exercise 4 🟢 easy choose
Your company is starting a new production API today. Should it target the newest Current release or an LTS release? And how would you make sure every developer uses the same version?
Show solution
An LTS release: it gets bug and security fixes for about 30 months, while Current lines change quickly and odd-numbered ones never become LTS. Pin the version in a .nvmrc (or .node-version) file read by version managers, and document it in package.json's "engines" field.
Exercise 5 🟡 medium predict the output
In what order are the lines printed, and why?
Show solution
The timer is due almost immediately, but its callback can only run when the main thread is free. The synchronous code (B, the 20 ms loop, C) runs to completion first; then the event loop runs the timer.
Exercise 6 🟡 medium build it · playground
Create playground/check-node.js: exit with code 1 and an error message on stderr if the Node major version is lower than a minimum given in a constant MIN = 22; otherwise print OK: Node 26 ≥ 22 (with the real numbers).
Show solution
Errors go to console.error (stderr) and a non-zero exit code tells scripts and CI that something failed (Chapter 2).
Exercise 7 🟡 medium explain why
A colleague says: "Node is single-threaded, so it can only handle one HTTP request at a time." Explain what's right and what's wrong with this statement, using the waiter analogy.
Show solution
Right: your JavaScript runs on one thread, so only one callback executes at any instant. Wrong: requests are not handled "one at a time" from start to finish. While one request waits for the database or the disk, Node starts on others, like a waiter who pins an order in the kitchen and serves the next table. Thousands of requests can be in progress at once; only the short bursts of JavaScript between waits take turns. (Behind the scenes, libuv also uses OS facilities and a thread pool to do the actual I/O.)
Exercise 8 🟡 medium build it
Write runtime.js with a function describeRuntime() that returns "browser" if window and document exist, "node" if process.versions.node exists, and "unknown" otherwise. Print the result.
Show solution
Use typeof for the checks: reading an undeclared global like window directly would throw a ReferenceError in Node.
Exercise 9 🔴 hard measure it
Show the cost of blocking: start a setInterval that should tick every 10 ms, block the thread for 100 ms after 25 ms, and print how many ticks happened in the first 200 ms. Explain why it's fewer than 20.
Show solution
An interval doesn't "catch up" on missed ticks. During the 100 ms block no callbacks can run, so roughly ten ticks are simply lost. In a server, the same block would delay every request in flight by 100 ms.
Exercise 10 🔴 hard mini-project · playground
Create playground/hello-server.js: an HTTP server that answers / with Hello!, /time with JSON { "uptimeSeconds": … } (use process.uptime()), and anything else with status 404. Make the script test itself with fetch on a free port and print the three status codes, then close.
Show solution
writeHead returns the response object, so you can chain .end(). You'll build a proper router with JSON bodies in Chapter 11.