The most honest JavaScript introduction, freely available online. Updated for ES2024.
Why this book
Eloquent JavaScript has been online for free since 2011. Haverbeke updates it with every major ECMAScript revision, no exception: the fourth edition covers ES2024. Most JavaScript books age in a drawer; this one has been a living document for fifteen years.
But what makes it different isn't the price or the update cadence. It's the tone. Haverbeke opens the introduction by admitting he quickly came to despise JavaScript the first time he used it. Then he explains, a few pages later, that he eventually came to like it. That arc is the whole book: start from honest confusion, follow it through to real understanding. There's no hand-waving, no shortcuts. He shows you the binary, then the assembly, then the JS — and you understand why languages exist.
The ideas that stay
1A program is a building of thought
The introduction shows the same program — add numbers 1 to 10 — written in raw binary, then readable assembly, then JavaScript's while loop, then sum(range(1, 10)). The moral is explicit: "A good programming language helps the programmer by allowing them to talk about the actions the computer has to perform at a higher level" (p. 8). This framing, early and concrete, answers the "why do languages exist" question better than most CS textbooks.
2Functions are values — the real rupture
In many languages, functions are special entities. In JavaScript, a function is a value like any other: you can store it in a variable, pass it as an argument, return it from another function. Haverbeke demonstrates this by storing a function in a binding called launchMissiles, then reassigning it to a no-op based on a condition:
let launchMissiles = () => { /* real launch */ }; if (safeMode) { launchMissiles = () => {}; // replaced by a no-op } launchMissiles(); // does nothing in safe mode
The fact that this is legal — and useful — is the conceptual key that unlocks the rest of the book.
3Closures: the environment travels with the function
When a function is created, it captures its surrounding scope. multiplier(2) returns a function that multiplies its argument by 2 — and that returned function keeps access to the factor binding even after multiplier has returned:
function multiplier(factor) { return number => number * factor; } let double = multiplier(2); double(5); // 10 — factor is still accessible
"A good mental model is to think of function values as containing both the code in their body and the environment in which they are created" (p. 72). This one concept is behind:
- event handlers (they capture the DOM element or state at the moment they're created);
- factory functions (each call produces a new function with its own private state);
- partial application (fix some arguments now, supply the rest later);
- a large portion of npm packages (most utility libraries are just closures over configuration).
4filter, map, reduce — thinking in transformations
Chapter 5 works through a dataset of human writing scripts (Latin, Arabic, Han...) using higher-order functions. The recipe analogy lands well: detailed step-by-step instructions vs. "soak, simmer, chop, add" — the second requires understanding a few extra words, but says far more in less space. The final example: computing the average year of origin for living scripts fits in a single readable expression:
SCRIPTS.filter(s => s.living).map(s => s.year)
Select, transform, aggregate. That's what thinking in transformations looks like.
5Prototypes and the secret life of this
JavaScript objects are linked to other objects through a prototype chain. When you access a property that an object doesn't have, the runtime walks up the chain until it finds it or reaches null. That's why arrays have .map() and .filter() without you ever defining them — those methods live on Array.prototype, one step up the chain:
Object.getPrototypeOf([]) == Array.prototype // true Object.getPrototypeOf(Array.prototype) == Object.prototype // true Object.getPrototypeOf(Object.prototype) // null
The chapter also clarifies why this behaves differently in arrow functions vs regular functions — arrow functions inherit this from their surrounding scope, regular functions get a new this decided by the caller. That's the source of more confusion than any other single JS feature.
6JavaScript is deliberately liberal — this has costs
The language accepts almost anything you type and interprets it in ways you didn't intend. Handy to start with, treacherous at scale:
counter = 0 // missing "let": global created SILENTLY
0 == false // true (automatic conversion)
0 === false // false (=== never converts: always prefer it)
"5" - 1 // 4 ("5" becomes a number)
"5" + 1 // "51" (1 becomes text!) — the classic trap
A NaN ("not a number", the result of an invalid operation like "abc" * 2) then propagates silently until you get wrong output with no error message. Haverbeke names these clearly: "Finding the source of such problems can be difficult" (p. 192). The fix (strict mode, === everywhere, TypeScript) is presented honestly, not evangelized.
7From callback hell to async/await
JavaScript runs on a single thread: it can't block waiting for a network response without freezing the whole page. The chapter walks the full history of how that problem was solved:
- Callbacks — you pass a function to be called when the result arrives. Simple, but nesting five of them to sequence five operations produces an unreadable pyramid.
- Promises — a receipt for a future value. Instead of nesting, you chain:
.then().then().catch()stays flat. Errors propagate automatically. - async/await — syntax sugar over Promises that reads like synchronous code.
const data = await fetch(url)looks immediate; the engine handles the Promise underneath.
"An async function implicitly returns a promise and can, in its body, await other promises in a way that looks synchronous" (p. 283). You still need to understand Promises underneath to debug what goes wrong, but async/await is where you live day to day.
8Modules: LEGO instead of mud
Before ES modules (2015), all JavaScript ran in a shared global scope, and any two scripts could overwrite each other's variables without warning. The module flips that: each file chooses what it exposes and what it borrows.
// cart.js: this file decides what gets out, the rest stays private
export function total(lines) { ... }
const VAT = 0.2 // invisible outside: no collision possible
// invoice.js: borrow it by name, the well-defined connector
import { total } from './cart.js'
Haverbeke calls this "LEGO, where pieces interact through well-defined connectors, and less like mud, where everything mixes with everything else" (p. 250): export and import are those connectors. npm ships over three million packages, "a large portion of those are rubbish, to be fair" (p. 254). The honest caveat about npm quality is characteristic of the book's tone.
Three things I didn't know before reading it
- Haverbeke uses a crow named Carla to teach async programming. She's hacking a WiFi by timing attack — testing each digit of the passcode and measuring how quickly the access point rejects it. The whole chapter (callbacks → Promises → async/await) flows through this single absurd narrative. It's the best pedagogical device in any programming book I've read.
- The introduction shows the same program (sum 1 to 10) in binary, pseudo-assembly, imperative JS, and finally
sum(range(1, 10)). What looks like a warmup is actually the deepest argument in the book: a language that lets you name concepts gives you power that raw instructions can't. It takes three pages and it sticks forever. - The book's license is Creative Commons. Haverbeke releases every edition for free online because he genuinely thinks that's the right thing to do. He also sells a paper version — with a bonus chapter — for those who want to support the work. That model has kept the book alive and updated for fifteen years.
My take, honestly
Haverbeke has a selling point nobody else has: he admits he hated JavaScript before loving it (p. 9). So he starts exactly where you start. And his first twelve chapters climb in the right order: functions as values, closures, filter/map/reduce, prototypes. Each concept rests on the previous one, and the exercises don't ask you to copy: they leave you to figure it out, which is where the learning happens.
The browser chapters (13 to 19) are fine but less essential: if your goal is understanding JS, the first twelve are enough. The platform game in chapter 16 is fun, but three chapters for one game is an investment. Diagonal reading allowed, I won't tell anyone.
In 2026, AI generates JavaScript by the kilometer, and that's exactly why you need closures and the prototype chain: they're what lets you review that code and spot the surprises. React itself is just closures in a nice outfit (hooks capture state). And the book that explains all this has been free for fifteen years. There is literally no excuse.
Odilon
Still relevant in 2026?
The fourth edition was published in 2024 and covers ES2024 explicitly. Private class fields (#property), optional chaining (obj?.prop), structuredClone, top-level await — it's all there. The fundamentals chapter (closures, prototypes, event loop) are as relevant as ever because frameworks are built on top of them, not instead of them. The only part that ages is the DOM/browser section, which is still accurate but less representative of how frontend code is written today.
Who is it for?
Read it if
- You write JavaScript but feel like you're guessing why closures,
this, or Promises behave the way they do - You're starting out and want a single book that takes you from zero to Node.js, for free
- You review AI-generated JS and want a solid framework to judge it
- You like books that assume you're an adult: no hand-holding, real exercises
Skip it if
- You already understand closures, the prototype chain, and the event loop — you'll know by chapter 3
- You want a reference you can index and query, not a book you read: the MDN docs serve that role better
- You're looking for React, Vue, or TypeScript specifically — this book doesn't cover frameworks or typed JS
For going further
The closures and prototypes chapters pair directly with the JavaScript course on this site. For TypeScript (the typed layer above JS), Effective TypeScript by Dan Vanderkam is the natural next step. For modern async patterns in practice, the REST API course covers fetch and async/await in a real context.
Comments (0)