Scope of var, let and const
var is function scoped while let and const are block scoped, so a variable declared with var inside an if or a loop leaks into the whole function, whereas let and const stay inside their own curly braces. Besides scope, the three differ in how they behave during hoisting, whether they can be redeclared, and whether they can be reassigned.
Theory
TL;DR
varhas function scope,letandconsthave block scope.- All three are hoisted, but
varis initialised toundefined, whileletandconstsit in the TDZ until their declaration line and throw aReferenceError. - Redeclaration in the same scope:
varallows it,letandconstthrow aSyntaxError. - Reassignment:
varandletallow it,constforbids it, but mutating the object behind the reference is still allowed. - Modern practice:
constby default,letwhen needed,varonly in legacy code.
Quick example
function test() {
if (true) {
var x = 1;
let y = 2;
const z = 3;
}
console.log(x); // 1
console.log(y); // ReferenceError
console.log(z); // ReferenceError
}
test();x is available because var "ignores" the if block and is hoisted to the function level. y and z are visible only inside the if.
Scope: function versus block
| Keyword | Scope |
|---|---|
var | Function scope |
let, const | Block scope |
Function scope means the variable is visible everywhere in the function where it is declared, regardless of any {} block.
Block scope means the variable is visible only inside the block { ... } where it is declared: the body of an if, the body of a loop, any pair of curly braces.
Hoisting and the TDZ
All variables are hoisted to the top of their scope, but they behave differently.
| Keyword | Hoisted | Usable before the declaration? |
|---|---|---|
var | yes | yes, but the value is undefined |
let, const | yes | no, an error because of the TDZ (temporal dead zone) |
Example with var:
console.log(a); // undefined
var a = 10;JavaScript hoists the declaration var a, but not the assignment a = 10.
Example with let and const:
console.log(b); // ReferenceError
let b = 20;b is hoisted too, but it lives in the temporal dead zone (TDZ): from the start of the scope up to the actual declaration line. Any access in that window throws a ReferenceError.
Redeclaration and reassignment
| Keyword | Can it be redeclared? |
|---|---|
var | Yes, it overwrites the value |
let, const | No, SyntaxError |
var x = 1;
var x = 2; // allowed
console.log(x); // 2
let y = 1;
let y = 2; // SyntaxError| Keyword | Can the value be changed? |
|---|---|
var | Yes |
let | Yes |
const | No, but the object inside can be mutated |
let name = 'Maria';
name = 'Oleh'; // allowed
const user = { name: 'Maria' };
user.name = 'Oleh'; // allowed: we mutate the object
user = {}; // TypeError: the reference cannot be reassignedconst freezes the binding, that is the reference, not the contents of the object. To freeze the contents as well you need Object.freeze.
An example that shows every difference
function demo() {
console.log(a); // undefined
// console.log(b); // ReferenceError
// console.log(c); // ReferenceError
var a = 1;
let b = 2;
const c = 3;
if (true) {
var a = 10; // the same variable!
let b = 20; // a new variable
const c = 30; // a new variable
console.log(a, b, c); // 10 20 30
}
console.log(a, b, c); // 10 2 3
}
demo();var a leaked out of the block and overwrote the outer value, while let and const created their own local variables inside the block, so the outer b and c were left untouched.
Which one to use in practice
| Keyword | When to use it |
|---|---|
let | For variables whose value has to change |
const | By default, always, unless the reference has to change |
var | Never, only to maintain legacy code |
Summary table:
| Property | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes | Yes | Yes |
| Use before declaration | undefined | Error (TDZ) | Error (TDZ) |
| Redeclaration | Yes | No | No |
| Reassignment | Yes | Yes | No |
| Modern standard | Legacy | Recommended | Default |
Common mistakes
- Assuming that a
varinside anifor aforcreates a local variable. It actually belongs to the whole function and can overwrite the outer value. - Confusing hoisting with "no error".
letandconstare hoisted too, accessing them inside the TDZ simply fails with aReferenceError. - Thinking
constmakes the value immutable. It only forbids reassigning the variable; object fields and array elements can still be mutated. - Using
varin a loop together with asynchronous code: every iteration sees one shared variable, whereasletcreates a fresh binding per iteration. - Relying on
varredeclaration as a way to "refresh" a variable: it is a silent overwrite that hides bugs.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.