let vs const vs var in JavaScript: Complete Comparison
Compare let, const, and var in JavaScript: scoping, hoisting, TDZ, reassignment rules, and which one to use in modern code with examples.
let vs const vs var in JavaScript: Complete Comparison
JavaScript offers three ways to declare variables: var, let, and const. Although they look interchangeable, they differ in scope, hoisting behavior, and whether the binding can be reassigned. Choosing the right one prevents bugs and makes intent obvious to anyone reading your code.
Quick comparison table
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes, initialized to undefined | Yes, but in TDZ | Yes, but in TDZ |
| Redeclarable in same scope | Yes | No | No |
| Reassignable | Yes | Yes | No (binding is fixed) |
| Must initialize at declaration | No | No | Yes |
| Creates property on global object | Yes | No | No |
var: function-scoped and hoisted
var is scoped to the nearest enclosing function (or the global scope). It ignores block boundaries like if or for. Declarations are hoisted to the top of the scope and automatically initialized to undefined, so you can reference them before the line they appear on without a ReferenceError.
console.log(x); // undefined (not an error)
var x = 5;
if (true) {
var y = 10;
}
console.log(y); // 10 — leaks out of the block
var y = 20; // redeclaration is allowed
Because var leaks out of blocks and silently redeclares, it is easy to introduce subtle bugs — the classic example being a loop counter that all callbacks share.
let: block-scoped and reassignable
let is scoped to the nearest enclosing block (anything between { }). It is hoisted but not initialized, so accessing it before its declaration throws a ReferenceError — this window is called the temporal dead zone (TDZ). You can reassign a let variable, but you cannot redeclare it in the same scope.
console.log(a); // ReferenceError — a is in the TDZ
let a = 1;
a = 2; // OK, reassignment is fine
let a = 3 // ← this line is a parse-time SyntaxError; NONE of the code above runs when it is present; // SyntaxError — cannot redeclare
if (true) {
let b = 10;
}
console.log(b); // ReferenceError — b does not exist here
const: block-scoped, immutable binding
const behaves like let for scoping and TDZ, but the binding cannot be reassigned after initialization. You must provide a value at the point of declaration. Crucially, const freezes the binding, not the value. If the value is an object or array, its contents are still mutable.
const PI = 3.14159;
PI = 3; // TypeError — assignment to constant
const user = { name: 'Ada' };
user.name = 'Grace'; // OK — mutating the object
user.age = 30; // OK — adding a property
user = { name: 'Lin' }; // TypeError — rebinding is forbidden
const nums = [1, 2, 3];
nums.push(4); // OK — [1, 2, 3, 4]
If you need a truly immutable object, wrap it with Object.freeze() — const alone is not enough.
The temporal dead zone explained
The TDZ is the region between the start of a block and the line where a let or const is declared. During that region the variable exists in scope but cannot be read or written. This design surfaces bugs early: instead of silently returning undefined like var, the engine throws a clear ReferenceError.
{
// TDZ for value starts here
// typeof value; // ReferenceError
let value = 42; // TDZ ends
console.log(value); // 42
}
Hoisting side by side
function demo() {
console.log(v); // undefined — var is hoisted and initialized
console.log(l); // ReferenceError — let in TDZ
var v = 1;
let l = 2;
}
The loop closure trap
This is the canonical reason to prefer let over var:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3 — all callbacks share the same i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 0, 1, 2 — each iteration gets a fresh binding
Which one should you use?
- Use
constby default. It communicates that the binding will not change, which is true for most variables in modern code. - Use
letwhen you genuinely need to reassign — loop counters that cannot usefor..of, accumulators, or values that swap between two states. - Avoid
varin new code. It exists for backward compatibility. The only common reason to reach for it is maintaining a legacy codebase that already uses it consistently.
Following the const-by-default rule pushes you toward smaller, single-purpose variables and makes accidental reassignment impossible — which is exactly the kind of guardrail JavaScript needed.