var vs let vs const: Which One Should You Actually Use?
Breaking down how var, let, and const really differ — scope, hoisting, the temporal dead zone — and building a practical rule of thumb for which one to reach for.
Why Do We Even Have Three Ways to Declare a Variable?
JavaScript started out with just var. After years of real-world use, people kept running into behavior that didn't match their intuition — you'd write something that looked perfectly reasonable and still get bitten. So ES6 (2015) introduced let and const, specifically to fix the parts of var's design that hadn't been thought through carefully enough.
So instead of treating these as three parallel options, it helps to think of it this way: let and const are the fix, var is the legacy behavior. Understanding what's wrong with var is really understanding why let and const were designed the way they were.
Scope Works Differently
The first key difference is scope.
var is function-scoped: only a function boundary limits its visibility. Blocks like if, for, or a bare {} don't contain it at all.
function checkVar() {
if (true) {
var message = 'hello'
}
console.log(message) // hello — it leaked right out of the if block
}let and const are block-scoped: once a {} wraps around them, they're unreachable outside that block.
function checkLet() {
if (true) {
let message = 'hello'
}
console.log(message) // ReferenceError: message is not defined
}This difference shows up most clearly in a for loop. Take a guess — what does this log?
for (var i = 1; i <= 3; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
// logs: 4 → 4 → 4That's because var i is a single variable shared across the whole function. By the time the loop finishes, i has already become 4, so by the time each setTimeout callback actually runs, they're all reading that same 4.
What if you swap var for let?
for (let i = 1; i <= 3; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
// logs: 1 → 2 → 3The clever part about let is that every iteration of the loop creates a brand-new i, scoped to that iteration's block. Each of the three callbacks closes over a different i, which is exactly why you get the order you'd intuitively expect.
Hoisting and the Temporal Dead Zone
For more detail, see the companion post on Hoisting
All three declaration types get hoisted — meaning the declaration itself is registered at the top of its scope — but what state they end up in afterward is completely different.
A hoisted var gets initialized to undefined right away, so reading it before the declaration line doesn't throw — you just get undefined:
console.log(a) // undefined
var a = 10let and const get hoisted too, but they land in a "declared but not yet initialized" state — that stretch is called the Temporal Dead Zone (TDZ). Accessing a variable while it's in the TDZ throws immediately:
console.log(b) // ReferenceError: Cannot access 'b' before initialization
let b = 10This is actually a good thing. var's "silently reads as undefined" behavior tends to bury bugs deeper — by the time something visibly breaks, you're often several lines removed from the actual cause. The TDZ surfaces the error the moment it happens, forcing you to put the declaration before its use.
Can You Redeclare It? Can You Reassign It?
var lets you redeclare the same variable name in the same scope with zero warnings:
var x = 1
var x = 2 // totally legal, no errorNeither let nor const allows redeclaring a name in the same scope:
let y = 1
let y = 2 // SyntaxError: Identifier 'y' has already been declaredAs for reassignment: let allows it, const doesn't:
let count = 1
count = 2 // fine
const max = 100
max = 200 // TypeError: Assignment to constant variable.Here's something a lot of people get wrong: what const locks down is the binding between the variable and its value, not the value's contents. If a const holds an object or array, you can still mutate what's inside it:
const user = { name: 'Alice' }
user.name = 'Bob' // fine — mutating the object's contents
user.age = 20 // fine — adding a new property works too
user = {} // TypeError — this is what's actually forbidden: swapping out the bound object entirelyIf you genuinely want the object's contents to be immutable too, you need Object.freeze() on top of const. Get this distinction straight now, and you won't be surprised later when you see someone push a new element into a const array.
Global Object Attachment
In a browser's top-level scope, a var-declared variable becomes a property on the window object. let and const don't do this.
var globalVar = 'foo'
let globalLet = 'bar'
console.log(window.globalVar) // 'foo'
console.log(window.globalLet) // undefinedThat means declaring var variables carelessly at the global level is effectively stuffing properties onto window — one careless name collision and you're clobbering some other library's code, or having yours clobbered. It's yet another way var gets you into trouble.
So Which One Should You Actually Reach For?
Pulling all of this together boils down to a pretty simple decision order:
- Default to
const. If a variable doesn't need reassigning after it's declared, useconst. It lets anyone reading the code see at a glance that this value never gets swapped out later, which cuts down the mental overhead of tracing the logic. - Reach for
letonly when you need reassignment — loop counters, accumulators that get overwritten, cases where you know upfront the value will change. varbasically has no place in new code. Its function-scoping, its tolerance for redeclaration, and the way it leaks ontowindowall carry far more risk than benefit in a modern JavaScript codebase. Unless you're maintaining legacy code or need to support truly ancient runtimes, there's no reason to reach for it.
Wrapping Up
On the surface, var, let, and const look like three ways to write the same thing: declaring a variable. Underneath, they trace an evolution in how JavaScript thinks about scope — from var's function-boundary, redeclaration-friendly, silently-global-leaking behavior, to let and const's block-boundary, redeclaration-blocking, TDZ-enforced approach. One rule is enough to remember: reach for const before let, and let before var.