var, let, const: Scoping Rules Every Developer Must Actually Understand
Function scope vs block scope, the temporal dead zone, and the classic closure-in-a-loop bug that var causes and let quietly fixes.
`var`, `let`, and `const` all declare variables. The difference that actually matters is scope — and it's the difference behind one of the most common real bugs in JavaScript history.
The classic bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Prints 3, 3, 3 — not 0, 1, 2
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0);
}
// Prints 0, 1, 2 — as expectedWhy this actually happens
`var` is function-scoped — there's exactly one `i`, shared across every loop iteration and every closure created inside it. By the time any `setTimeout` callback actually runs, the loop has already finished and `i` is `3`. `let` is block-scoped: each iteration of the loop gets its own fresh binding of `j`, so each closure captures a genuinely different variable, not a shared one.
The temporal dead zone
`let` and `const` declarations are hoisted like `var`, but they aren't initialized until the line that declares them runs — the gap between the start of the block and that line is the "temporal dead zone," and touching the variable inside it throws a `ReferenceError` instead of silently returning `undefined` the way `var` does. That's a deliberate design fix: `var`'s silent `undefined` used to hide real bugs; `let`/`const` turn the same mistake into a loud, immediate error.
The practical rule
Default to `const`. Use `let` only for a variable you genuinely intend to reassign — a loop counter, an accumulator. There's essentially no remaining reason to write `var` in new code; it exists in every modern codebase purely for backward compatibility with code written before 2015.
The snippets above are the short version — the full, runnable code lives on GitHub.
Part 5 of the JavaScript fundamentals series.