Q&A
let vs. var vs. const in JavaScript
Overview
var is the legacy declaration keyword - its function scope and hoisting to undefined cause subtle bugs (the classic "loop variable leak," the "why is it undefined" mystery). ES6 introduced let and const with block scoping and the Temporal Dead Zone (TDZ) to make these bugs both visible and avoidable.
graph TD
A[Declare a variable] --> B[const: block scope, no reassign - default choice]
A --> C[let: block scope, can reassign]
A --> D[var: function scope, hoisted - avoid]
B --> E["Use when value won't change"]
C --> F["Use for counters, changing refs"]
D --> G["Legacy; block-scope bugs"]
style B fill:#1e1b4b,color:#fff
Scope
var - function scope
function example() {
if (true) {
var x = 10; // declared in if block, but...
}
console.log(x); // 10 - var leaks out of the block!
}
var is scoped to the enclosing function (or global if at the top level). It escapes if, for, and any other block - a major source of bugs.
let and const - block scope
function example() {
if (true) {
let y = 20;
const z = 30;
}
console.log(y); // ❌ ReferenceError - block-scoped, not visible here
}
Block scope is what most programmers intuitively expect.
Hoisting
All three are hoisted (the name is registered at compile time), but differently:
console.log(a); // undefined - var hoisted, initialized to undefined
console.log(b); // ❌ ReferenceError - let in TDZ
console.log(c); // ❌ ReferenceError - const in TDZ
var a = 1;
let b = 2;
const c = 3;
var- hoisted AND initialized toundefined. Accessible (asundefined) before the line.let/const- hoisted but NOT initialized (Temporal Dead Zone). Accessing before the line throwsReferenceError.
The TDZ is intentional - it turns the silent undefined bug of var into a loud, catchable error.
Re-assignment and Re-declaration
var a = 1;
var a = 2; // OK - var can be re-declared in the same scope
a = 3; // OK - var can be reassigned
let b = 1;
let b = 2; // ❌ SyntaxError - no re-declaration in same scope
b = 3; // OK - let can be reassigned
const c = 1;
const c = 2; // ❌ SyntaxError
c = 3; // ❌ TypeError - const cannot be reassigned
const Does NOT Mean Immutable
const prevents reassignment of the binding - it doesn't freeze the object or array:
const person = { name: "Ada" };
person.name = "Linus"; // ✓ - mutating the object is allowed
person = {}; // ❌ TypeError - can't reassign person itself
const arr = [1, 2, 3];
arr.push(4); // ✓ - mutating the array is fine
arr = []; // ❌ TypeError
For deep immutability, use Object.freeze() (shallow) or libraries.
The Classic var Loop Bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3 3 3 (var leaks - all closures share the same i)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0 1 2 (let creates a new binding per iteration)
This is the canonical example of why var in loops causes bugs. let creates a new scoped i for each iteration.
Summary Table
| Feature | var |
let |
const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes (as undefined) |
Yes (TDZ) | Yes (TDZ) |
| Re-declarable | Yes | No | No |
| Re-assignable | Yes | Yes | No |
| Use today | ❌ Avoid | ✓ When re-assigning | ✓ Default choice |
Why This Matters in Interviews
This is one of the most common JavaScript questions. Hit all three scope differences, the var-hoisting-to-undefined vs TDZ behavior of let/const, the loop-closure classic bug, and that const doesn't mean immutable (objects/arrays are still mutable). The recommendation - const by default, let when needed, never var - signals a modern JS mindset.
Common Pitfalls
- Thinking
constmakes an object immutable (it prevents rebinding, not mutation). - Expecting
let/constto returnundefinedbefore declaration (they throw ReferenceError - TDZ). - The
varloop-closure bug (all iterations share one variable). - Re-declaring a
let/constin the same block (SyntaxError). - Declaring loop counters with
constwhen you need to increment them (uselet).