Problem Solved

JavaScript · var/let/const

Home › Courses › JavaScript › Problems › var/let/const
🟨 Concept · 4 min read

var vs let vs const.
Scope, reassignment, hoisting.

Three keywords that all declare variables. They differ in scope, in whether they can be reassigned, and in how they interact with hoisting. The short version: use const by default, let when you need to reassign, and never var. This page covers each difference with real examples, the classic loop bug, and the reason const doesn't mean immutable.

Short answer: var is function-scoped, reassignable, hoisted to undefined. let is block-scoped, reassignable, hoisted to a temporal dead zone (accessing before declaration throws). const is block-scoped, not reassignable, also in the TDZ until declaration. Use const by default, let when you need to reassign. const doesn't mean immutable — object properties and array elements can still be changed.

✅ The three differences

Scope, reassignment, hoisting.

Scope

var = function or global. let/const = block (the nearest { }).

Reassignment

var/let = yes. const = no (binding can't be reassigned).

Hoisting

var = hoisted to undefined. let/const = hoisted but in TDZ until declaration.

Default choice

const first, let when needed, var never. This is the modern standard.

✅ The classic loop bug

The most common "why doesn't this work?" with var.

var-loop.js
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}

// What does this log? 0, 1, 2?
// Or 3, 3, 3?
Actual output:
3
3
3

Why: var is function-scoped. All three callbacks
close over the SAME i variable — the one in the
function scope. By the time the callbacks run
(after the loop finishes), i is 3.

❌ The callback doesn't capture the value of i
at the moment of creation.
❌ It captures the variable, which has since
been mutated to 3.
❌ This is the "closure in a loop" bug.

Fix: use let. Each iteration gets its own i.
let-loop.js
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}

// Now: 0, 1, 2
Actual output:
0
1
2

Why: let is block-scoped. Each iteration of
the loop creates a new binding for i — a new
variable in a new block. The callbacks each
close over their own i.

✅ 0, 1, 2 — the expected behavior.
✅ No closure bug.
✅ No workaround needed.

This is why let exists. It fixes the loop bug
that made var frustrating for 20 years.

The fix without let (for legacy code):
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 100);
})(i);
}

The IIFE creates a new scope per iteration —
captures the value of i at call time. Works,
but verbose. let is cleaner.

✅ const is not immutable

The binding can't change. The value can.

const-mutation.js
// const with primitives — can't reassign
const x = 5;
x = 6;
// ❌ TypeError: Assignment to constant variable

// const with objects — can mutate properties
const user = { name: 'Alice' };
user.name = 'Bob'; // ✅ works
user.email = 'bob@example.com'; // ✅ works
user = { name: 'Charlie' }; // ❌ TypeError

// const with arrays — can mutate elements
const list = [1, 2, 3];
list.push(4); // ✅ works
list[0] = 10; // ✅ works
list = [4, 5, 6]; // ❌ TypeError

// To make an object truly immutable:
const frozen = Object.freeze({ name: 'Alice' });
frozen.name = 'Bob'; // ❌ silently fails (or throws in strict mode)
What const actually prevents:

❌ Reassigning the binding:
user = newObject
list = newArray

✅ Mutating the value the binding points to:
user.name = 'Bob'
user.email = '...'
list.push(4)
list[0] = 10

This is why const is the default for objects
and arrays — you're saying "I won't rebind this
variable," not "this is immutable."

For real immutability:
✅ Object.freeze() — shallow freeze
✅ Spread to create new objects:
const newUser = { ...user, name: 'Bob' }
✅ Immutable libraries (Immer, Immutable.js)

Most of the time, const + careful mutation is
enough. Use Object.freeze() only when you need
to enforce immutability for a specific reason.

Don't use var in new code. It has no advantages over let and const. Its function scoping causes the loop closure bug, its hoisting behavior (initialized to undefined) hides bugs, and its redeclaration behavior (you can declare the same variable twice in the same scope) is confusing. Modern JavaScript linting rules (no-var in ESLint) ban it. The only reasons to use var are maintaining legacy code or meeting a specific compatibility requirement — neither applies to new projects in 2026.

⚠️ Common mistakes

The var/let/const mistakes that cost real time.

1. Using var in loops with closures

All callbacks share the same variable. Fix: use let.

2. Expecting const to freeze objects

The object is still mutable. Fix: use Object.freeze() or spread for immutability.

3. Accessing let/const before declaration

Temporal dead zone — throws ReferenceError. Fix: declare before use.

4. Redeclaring with let/const in the same scope

SyntaxError: Identifier has already been declared. Fix: use a new variable name.

5. var inside a block

Leaks out — var is function-scoped, not block-scoped. Fix: use let.

6. Reassigning const object thinking it works

Throws TypeError. Fix: use let, or spread to a new object.

7. Mixing var and let in the same scope

Redeclaration error or silent shadowing. Fix: pick one — const/let.

8. Global variables with var

Attach to window, cause collisions. Fix: use const/let in modules.

❓ Frequently asked

What is the difference between var, let, and const?

Three differences. (1) Scope: var is function-scoped, let and const are block-scoped. (2) Reassignment: var and let can be reassigned, const cannot. (3) Hoisting: all three are hoisted, but var is initialized to undefined, while let and const are in the temporal dead zone until their declaration. In modern code, use const by default, let when reassignment is needed, never var.

Does const mean the value can't be changed?

No — const means the binding can't be reassigned, not that the value is immutable. A const primitive cannot be reassigned. But a const object can have its properties changed, and a const array can be mutated. Only reassignment of the whole binding (obj = newObj) is forbidden. To make an object truly immutable, use Object.freeze().

Why does var behave differently in loops?

Because var is function-scoped, not block-scoped. A var declared inside a for loop is scoped to the function that contains the loop, not to the loop body. So every iteration shares the same variable. This is the classic 'closure in a loop' bug. Fix: use let (block-scoped), or wrap the body in a function to create a new scope per iteration.

What is the temporal dead zone?

The temporal dead zone (TDZ) is the period between entering a scope and the declaration of a let or const variable. During this period, accessing the variable throws a ReferenceError, even though the variable is technically hoisted. This is intentional — it catches the bug of using a variable before declaring it. var doesn't have this — accessing a var before declaration returns undefined instead of throwing.

Should I ever use var?

In modern code (ES2015+), no. Use const by default and let when you need to reassign. var has no advantages that let/const don't cover, and its function-scoping causes bugs. The only reasons to use var are: (1) maintaining legacy code, (2) working in environments where var is required. For anything new, use const and let.

Can I use let and const in older browsers?

let and const are supported in all modern browsers since 2015. If you need to support IE11, you'll need a transpiler like Babel to convert let/const to var. In 2026, this is almost never needed — IE11 is end-of-life and has less than 0.1% market share.

🔗 Continue learning

🗺️ JavaScript learning path
The full roadmap from variables to async — every concept in order

Next in this series:

🔒 What is a closure?
A function that remembers its outer variables — the pattern and the gotcha
← All JavaScript problems
Problem solved · JavaScript