var vs let vs const in JavaScript: Scope and Hoisting
Basics4 minInnovialabs editorial team
In short
Use const by default and let when the variable must be reassigned; avoid var, because it is function-scoped and hoisted with undefined, which causes subtle bugs.
The short answer
const— block-scoped, cannot be reassigned. Use it by default.let— block-scoped, can be reassigned. Use it for counters, accumulators and values that really change.var— function-scoped, hoisted with the valueundefined, can be redeclared. Avoid it in new code.
| var | let | const | |
|---|---|---|---|
| Scope | function | block | block |
| Reassign | yes | yes | no |
| Redeclare in same scope | yes | no | no |
| Before declaration | undefined | ReferenceError (TDZ) | ReferenceError (TDZ) |
| Must be initialized | no | no | yes |
Function scope vs block scope
A block is anything in curly braces: if, for, while, or a bare { }. let and const live only inside the block. var ignores blocks and belongs to the whole function (or the global scope).
function demo() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1 — var leaked out of the if block
console.log(b); // ReferenceError: b is not defined
}
The leak of var is the root of most problems: a variable you thought was local to a loop or condition is visible and changeable across the entire function.
Hoisting and the temporal dead zone
Hoisting means the engine registers declarations before executing the code of their scope. All three keywords are hoisted, but differently:
varis created and initialized withundefinedright away.letandconstare created but not initialized. Until the line with the declaration runs, the variable is in the temporal dead zone (TDZ), and any access throws aReferenceError.
console.log(x); // undefined
var x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
The TDZ is a feature: reading a variable before it has a value is almost always a bug, and let/const report it loudly instead of silently giving you undefined.
Function declarations are hoisted fully, with their body, so you can call them above their definition. Function expressions assigned to const follow the TDZ rules.
The classic loop closure bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3, 3, 3
There is only one i for the whole function. The callbacks run after the loop finishes, and by then i is 3.
With let, each iteration gets a fresh binding, so every callback captures its own value:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0, 1, 2
Before let existed, developers wrapped the body in an immediately invoked function to create a new scope. Today let solves it in one word.
const does not freeze objects
const protects the binding, not the value. You cannot assign a new object to the variable, but you can change the object it points to.
const user = { name: "Aziz" };
user.name = "Dilnoza"; // OK — the object is mutated
user = {}; // TypeError: Assignment to constant variable
const list = [1, 2];
list.push(3); // OK
If you need an immutable object, use Object.freeze() (shallow) or design your code to create new objects instead of mutating.
A simple rule
- Write
constfirst. - Change it to
letonly when you actually need to reassign the variable. - Do not use
varin new code. Linters can enforce this (for example, theno-varandprefer-construles in ESLint).
This also makes code easier to read: when you see let, you know the value changes somewhere below.
Common mistakes
- Thinking
constmakes arrays and objects read-only. - Using
varinside loops with asynchronous callbacks. - Declaring
letinside a block and expecting it to be visible after the block. - Using a variable at the top of a function "because it is hoisted" — with
let/constthis throws. - Redeclaring a
varby accident in a long function and overwriting a value.