var vs let vs const
The Short Answer
var is function-scoped, hoisted with initialization to undefined, and can be redeclared. let is block-scoped, hoisted but not initialized (temporal dead zone), and cannot be redeclared. const is like let but also cannot be reassigned after initialization. In modern JavaScript, use const by default, let when you need reassignment, and avoid var entirely.
Scope
The most important difference is scope. var is scoped to the nearest function (or global if not in a function). let and const are scoped to the nearest block — any pair of curly braces (if, for, while, or even a standalone {}). This means var leaks out of blocks, which is a common source of bugs.
function example() {
if (true) {
var varVariable = 'I leak out';
let letVariable = 'I stay here';
const constVariable = 'I stay here too';
}
console.log(varVariable); // 'I leak out' — var is function-scoped
console.log(letVariable); // ReferenceError — let is block-scoped
console.log(constVariable); // ReferenceError — const is block-scoped
}
// The classic for-loop problem
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 — var is shared across all iterations!
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// Logs: 0, 1, 2 — let creates a new binding per iteration
The for-loop example is the classic interview question. With var, there's one shared i variable for all iterations — by the time the timeouts fire, the loop has finished and i is 3. With let, each iteration gets its own j binding, so each closure captures a different value.
Hoisting
All three declarations are hoisted (the engine knows about them before execution reaches the declaration line). But they differ in what happens when you access them before the declaration.
var hoisting
var declarations are hoisted AND initialized to undefined. You can access the variable before its declaration line — you just get undefined instead of an error. This is confusing and hides bugs.
console.log(name); // undefined (not an error!)
var name = 'Alice';
console.log(name); // 'Alice'
// What the engine actually does:
// var name = undefined; ← hoisted and initialized
// console.log(name); ← undefined
// name = 'Alice'; ← assignment stays in place
// console.log(name); ← 'Alice'
let/const hoisting (Temporal Dead Zone)
let and const are hoisted but NOT initialized. The period between entering the scope and reaching the declaration is called the Temporal Dead Zone (TDZ). Accessing the variable during the TDZ throws a ReferenceError. This is safer — it catches bugs where you accidentally use a variable before it's ready.
// Temporal Dead Zone for 'let'
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 25;
// Temporal Dead Zone for 'const'
console.log(PI); // ReferenceError: Cannot access 'PI' before initialization
const PI = 3.14159;
// The TDZ exists even within the same block:
{
// TDZ starts here for 'x'
console.log(x); // ReferenceError
let x = 10; // TDZ ends here
console.log(x); // 10
}
Redeclaration
var allows redeclaring the same variable in the same scope — the second declaration is silently ignored (just becomes an assignment). let and const throw a SyntaxError if you try to redeclare in the same scope. This prevents accidental variable shadowing bugs.
// var — allows redeclaration (dangerous)
var user = 'Alice';
var user = 'Bob'; // No error — silently overwrites
console.log(user); // 'Bob'
// let — prevents redeclaration
let count = 0;
let count = 1; // SyntaxError: Identifier 'count' has already been declared
// const — same as let
const MAX = 100;
const MAX = 200; // SyntaxError
// But shadowing in a nested block is fine for let/const:
let value = 'outer';
{
let value = 'inner'; // Different scope — this is shadowing, not redeclaration
console.log(value); // 'inner'
}
console.log(value); // 'outer'
Reassignment (const)
const prevents reassignment — you cannot point the variable at a different value after initialization. But it does NOT make the value immutable. If the value is an object or array, you can still modify its contents. const only locks the binding (the variable name), not the value it points to.
// const prevents reassignment
const name = 'Alice';
name = 'Bob'; // TypeError: Assignment to constant variable
const count = 0;
count++; // TypeError: Assignment to constant variable
// But const does NOT prevent mutation of objects/arrays!
const user = { name: 'Alice', age: 30 };
user.name = 'Bob'; // ✅ Works — mutating the object's property
user.age = 31; // ✅ Works
user = { name: 'Eve' }; // ❌ TypeError — can't reassign the variable
const items = [1, 2, 3];
items.push(4); // ✅ Works — mutating the array
items[0] = 99; // ✅ Works
items = [5, 6]; // ❌ TypeError — can't reassign
// For true immutability, use Object.freeze (shallow) or structuredClone
const frozen = Object.freeze({ name: 'Alice' });
frozen.name = 'Bob'; // Silently fails (or TypeError in strict mode)
const means constant binding, not constant value
Think of const as "this variable name will always point to this thing." The thing itself can still change internally (if it's an object). For primitives, const effectively means immutable because you can't change a primitive without reassignment.
Full Comparison
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes + initialized to undefined | Yes but TDZ (ReferenceError) | Yes but TDZ (ReferenceError) |
| Redeclaration | Allowed | SyntaxError | SyntaxError |
| Reassignment | Allowed | Allowed | TypeError |
| Must initialize? | No | No | Yes — must assign at declaration |
| Global object property? | Yes (window.x) | No | No |
Best Practices
Modern JavaScript conventions
- ✅Use `const` by default — signals the binding won't change, makes code easier to reason about
- ✅Use `let` only when you need to reassign (loop counters, accumulators, conditionally assigned values)
- ✅Never use `var` — it has no advantages over let/const and its scoping rules cause bugs
- ✅If a linter flags a `let` that's never reassigned, change it to `const`
Why Interviewers Ask This
This is a foundational JavaScript question that tests your understanding of scope, hoisting, and the temporal dead zone. Interviewers often follow up with the classic for-loop closure problem or ask you to predict output of code that mixes var and let. It reveals whether you understand JavaScript's execution model and can explain why modern code avoids var. It's also a quick signal of whether you write modern JavaScript or are stuck in pre-ES6 patterns.
Quick Revision Cheat Sheet
var: Function-scoped, hoisted + undefined, redeclarable — avoid
let: Block-scoped, TDZ, no redeclaration, reassignable
const: Block-scoped, TDZ, no redeclaration, no reassignment
const + objects: Binding is locked, but object contents can still be mutated
Default choice: const > let > var (never use var)
For-loop trap: var shares one binding across iterations; let creates one per iteration