var, let, const and Scope: What Actually Changes
If you've looked at JavaScript written before roughly 2015 and JavaScript written today, one difference jumps out immediately: var used to be everywhere, and now you mostly see let and const. Understanding why is one of the fastest ways to stop being confused by bugs that don't seem to make sense.
What "scope" means
Scope is just the answer to one question: "From where in my code can I access this variable?"
function greet() {
const message = "Hello!"
console.log(message) // works — we're inside the same function
}
console.log(message) // ❌ ReferenceError: message is not defined
message only exists inside greet. Outside of it, it's as if that variable never existed. That's scope: a variable is only visible within the "block" (loosely, the { } region) where it was created — and any blocks nested inside that one.
The core difference: function scope vs block scope
This is the actual difference between var and let/const — everything else follows from it.
varis function-scoped. It ignores{ }blocks likeifstatements andforloops entirely — it's only contained by the nearest function (or the global scope, if there's no function).letandconstare block-scoped. They respect{ }— a variable declared inside anifblock doesn't leak outside of it.
Here's the bug this causes in practice:
if (true) {
var x = 10
}
console.log(x) // 10 — var leaked out of the if block!
if (true) {
let y = 10
}
console.log(y) // ❌ ReferenceError — let stayed inside the block, as expected
This "leaking" is exactly why var causes confusing bugs in larger codebases: a variable you thought was temporary and local is actually still alive somewhere it shouldn't be.
The classic loop bug
This is the single most common example used to explain why var causes real problems:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// Logs: 3, 3, 3 (not 0, 1, 2, as you'd expect!)
Because var doesn't create a new i for each loop iteration — there's just one i, shared across the whole loop, and by the time the setTimeout callbacks run, the loop has already finished and i is 3.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// Logs: 0, 1, 2 — as expected
let creates a fresh i for every iteration, so each callback captures its own separate value. This single behavior is a big part of why let replaced var as the default.
let vs const
Once you're block-scoped, the choice between let and const is simple: it's about whether you plan to reassign the variable, not whether its contents can change.
const name = "Ana"
name = "Beatriz" // ❌ TypeError — const cannot be reassigned
const user = { name: "Ana" }
user.name = "Beatriz" // ✅ totally fine — we're not reassigning `user`,
// we're mutating the object it points to
const doesn't mean "frozen forever" — it means "this variable will always point to the same value." If that value is an object or array, its contents can still change; you just can't point user at a different object entirely.
A practical rule
- Default to
constfor everything. - Switch to
letonly when you know the variable needs to be reassigned (a loop counter, a value that changes based on a condition). - Avoid
var— there's no situation in modern JavaScript where it does somethingletorconstcan't do better and more predictably.
This isn't a style preference — following it eliminates an entire category of bugs (the loop bug above, and its variants) before you ever have to debug them.