var vs let vs const in JavaScript
Short answer
var is function-scoped and hoisted as undefined. let and const are block-scoped and stay in the temporal dead zone until their declaration runs, so reading them early throws. let can be reassigned, const cannot — though a const object's contents remain mutable. Use const by default, let when you must reassign, and var never.
| var | let | const | |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes, initialised to undefined | Yes, but in the TDZ | Yes, but in the TDZ |
| Read before declaration | undefined | ReferenceError | ReferenceError |
| Reassignable | Yes | Yes | No |
| Redeclarable in the same scope | Yes | No | No |
| Must be initialised at declaration | No | No | Yes |
| Creates a property on globalThis | Yes, at top level | No | No |
| Per-iteration binding in a for loop | No — one shared binding | Yes | N/A (for…of only) |
Scope: function versus block
A var declaration belongs to the nearest enclosing function, no matter how deeply nested inside blocks it appears. An if, a for or a bare pair of braces does not contain it. let and const belong to the nearest enclosing block, which matches how almost every other language behaves and how people intuitively read indented code.
1function demo() {
2 if (true) {
3 var a = 1;
4 let b = 2;
5 const c = 3;
6 }
7 console.log(a);
8 // → 1 var escaped the if block
9 console.log(b);
10 // → ReferenceError: b is not defined
11 console.log(c);
12 // → ReferenceError: c is not defined
13}This is why var leaks. A temporary variable declared inside a loop or a branch stays alive for the rest of the function, so a later declaration with the same name silently overwrites it instead of creating something new. Block scoping makes the variable disappear at the closing brace, which is what you meant.
Hoisting and the temporal dead zone
All three declarations are hoisted, in the sense that the engine registers them when it enters the scope, before running any code. The difference is what the binding holds at that moment. A var binding is immediately initialised to undefined, so reading it early is legal and gives you undefined. A let or const binding exists but has no value yet, and any access before the declaration line throws a ReferenceError. That gap is the temporal dead zone.
1console.log(a);
2var a = 1;
3// → undefined hoisted and initialised
4
5console.log(b);
6let b = 2;
7// → ReferenceError: Cannot access 'b' before initialization
8// Note the wording — "cannot access", not "is not defined".
9// The engine knows b exists; it is just still in the TDZ.
10
11console.log(d);
12// → ReferenceError: d is not defined
13// Different message: this name was never declared at all.The two error messages are worth memorising, because the distinction proves the point. "Cannot access before initialization" only makes sense if the engine already knows the variable exists — which it does, because the declaration was hoisted. The TDZ is not the absence of hoisting; it is hoisting without initialisation.
1function f() {
2 const g = () => console.log(x); // no error yet — not called
3 let x = 5;
4 g();
5 // → 5 by the time g runs, x is initialised
6}
7
8function h() {
9 const g = () => console.log(y);
10 g();
11 // → ReferenceError: Cannot access 'y' before initialization
12 let y = 5;
13}
14// → the same code text, decided by WHEN it runs, not where it sitsconst locks the binding, not the value
This is the most misunderstood point of the three, and one of the most commonly asked. const prevents reassigning the variable — you cannot make the name point at something else. It says nothing about the object the name points at. Properties can be added, changed and deleted freely, and arrays can be pushed to and sorted.
1const user = { name: "Arun" };
2user.name = "Karthikeyan"; // mutation — allowed
3user.city = "Chennai"; // adding — allowed
4console.log(user);
5// → { name: "Karthikeyan", city: "Chennai" }
6
7user = { name: "Someone" }; // reassignment
8// → TypeError: Assignment to constant variable.
9
10const nums = [1, 2];
11nums.push(3);
12console.log(nums);
13// → [1, 2, 3] the array changed; the binding did not
14
15const n = 1;
16n++;
17// → TypeError: Assignment to constant variable.The reason is the same reference rule that governs everything else in JavaScript: the variable holds a reference to the object, and const freezes the variable, not what it points to. If you want the object itself to resist change, that is Object.freeze — and note that freeze is shallow, so nested objects stay mutable unless you recurse.
1const config = Object.freeze({ retries: 3, db: { host: "local" } });
2
3config.retries = 5;
4console.log(config.retries);
5// → 3 silently ignored in sloppy mode, TypeError in strict mode
6
7config.db.host = "remote";
8console.log(config.db.host);
9// → "remote" freeze is one level deepThis is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299The loop closure classic
If one interview question is guaranteed to appear from this topic, it is this one. A for loop with var creates a single binding shared by every iteration, so every closure created inside the loop sees the same variable — and by the time those closures run, the loop has finished. let creates a fresh binding per iteration, which is a special rule the language added specifically for this.
1for (var i = 0; i < 3; i++) {
2 setTimeout(() => console.log(i), 0);
3}
4// → 3
5// → 3
6// → 3 one shared `i`, read after the loop ended
7
8for (let i = 0; i < 3; i++) {
9 setTimeout(() => console.log(i), 0);
10}
11// → 0
12// → 1
13// → 2 a new binding each iteration, each closure keeps its own1// An IIFE created a new function scope per iteration
2for (var i = 0; i < 3; i++) {
3 (function (j) {
4 setTimeout(() => console.log(j), 0);
5 })(i);
6}
7// → 0
8// → 1
9// → 2
10
11// Or bind the value as an argument
12for (var i = 0; i < 3; i++) {
13 setTimeout(console.log, 0, i);
14}
15// → 0 1 2 — same idea, the value is captured at call timeBeing able to explain why let works here is the follow-up that separates answers. It is not that let is block-scoped in general; it is a specific rule for for loops that copies the binding into each iteration. That is also why const fails in a classic for loop — the per-iteration copy would need to be reassigned by i++ — while const works fine in for…of, where each iteration binds a new value rather than incrementing one.
Redeclaration and the global object
var lets you declare the same name twice in one scope, which quietly hides typos and accidental reuse. let and const throw a SyntaxError at parse time, before anything runs. Separately, a top-level var creates a property on the global object, while let and const create bindings that live in the script scope and do not appear on globalThis.
1var x = 1;
2var x = 2; // fine, silently replaces
3console.log(x);
4// → 2
5
6let y = 1;
7let y = 2;
8// → SyntaxError: Identifier 'y' has already been declared
9// Thrown at parse time — nothing in the file runs.
10
11// At the top level of a classic script:
12var a = 1;
13let b = 2;
14console.log(globalThis.a, globalThis.b);
15// → 1 undefinedThe globalThis difference matters less than it used to, because modules have their own scope and nothing declared in a module lands on the global object anyway. It still appears in interview questions, and it explains why a stray top-level var in an old script could collide with a browser built-in.
Shadowing, and the one case that throws
Declaring a name in an inner scope that already exists outside it is shadowing, and it is legal for all three. The inner name wins for the length of its block, and the outer one is untouched. There is one combination that fails, and it is a favourite of quiz writers: a var inside a block cannot shadow a let of the same name in an enclosing scope, because var would try to escape the block into the scope where the let already lives.
1let count = 1;
2{
3 let count = 2;
4 console.log(count);
5 // → 2
6}
7console.log(count);
8// → 1 the outer binding was never touched
9
10function ok() {
11 var n = 1;
12 { let n = 2; console.log(n); }
13 // → 2 let may shadow var inside a block
14 console.log(n);
15 // → 1
16}
17
18function bad() {
19 let m = 1;
20 { var m = 2; }
21 // → SyntaxError: Identifier 'm' has already been declared
22 // var would hoist to the function scope, where m already exists
23}Shadowing is not a bug in itself — a callback parameter shadowing an outer variable is completely ordinary. It becomes a problem when it is accidental, which is another argument for block scoping: with let and const the shadow ends at the closing brace, so its reach is visible on the page.
What to use, and why the rule is that simple
Use const by default. Use let only when the variable genuinely has to be reassigned — a loop counter, an accumulator, a value built up across branches. Never use var in new code. The reason to start with const is not stylistic purity: a name that never changes is one less thing to track while reading, and reaching for let becomes a deliberate signal that this value moves.
| Situation | Use | Note |
|---|---|---|
| Any value you will not reassign | const | Includes objects and arrays you mutate |
| Loop counter in a classic for loop | let | const breaks on i++ |
| The loop variable in for…of | const | Rebound each iteration, never reassigned |
| Accumulator built across branches | let | Assigned in more than one place |
| A value assigned once, later in the function | let | const requires an initialiser |
| Anything at all in new code | Not var | Function scope and no TDZ, both undesirable |
| A true constant you must protect | const + Object.freeze | And recurse if it is nested |
Why does const fail in a for loop but work in for…of?
A classic for loop reassigns the counter with i++, and const forbids reassignment. In for…of and for…in, each iteration creates a brand-new binding initialised to the next value rather than reassigning the old one, so const is legal and is the conventional choice there.
Is var still useful anywhere?
Not in new code. Every var behaviour is either matched by let or is the behaviour you are trying to avoid. It remains in older codebases and in scripts written before ES6, so you need to read it — but there is no situation where writing it today is the better option.
Are function declarations hoisted like var?
More completely. A function declaration is hoisted along with its body, so you can call it before its line in the file. A function expression assigned to a var is not — the variable is hoisted as undefined, and calling it throws "is not a function".
Does the TDZ apply to function parameters?
Yes, in a sense. Default parameters are evaluated left to right, so a default that references a parameter declared later throws a ReferenceError. function f(a = b, b = 2) {} fails when called with no arguments, for exactly the same reason as the TDZ in a block.
What happens if I assign to an undeclared variable?
In sloppy mode it creates an implicit global, which is a long-standing source of bugs. In strict mode — and therefore in every ES module — it throws a ReferenceError. This is one of the main reasons to keep strict mode on, and modules give it to you automatically.
Frequently asked questions
- What is the difference between var, let and const?
- var is function-scoped and hoisted as undefined, so reading it early gives undefined. let and const are block-scoped and sit in the temporal dead zone until their declaration executes, so reading them early throws. let can be reassigned; const cannot, though a const object's contents can still be changed.
- What is the temporal dead zone?
- The period between entering a scope and executing a let or const declaration. The binding exists — it was hoisted — but has no value, so any access throws "Cannot access before initialization". It exists to turn a whole class of ordering mistakes into loud errors rather than silent undefined values.
- Are let and const hoisted?
- Yes. The engine registers them when it enters the scope, which is what hoisting means. The difference from var is initialisation: var is set to undefined immediately, while let and const are left uninitialised until their declaration runs. Saying they are not hoisted is the most common wrong answer to this question.
- Does const make a value immutable?
- No — it makes the binding constant. You cannot point the name at a different value, but you can freely mutate the object or array it refers to. For shallow immutability use Object.freeze, and recurse over the tree if you need it to be deep.
- Can I change a property of a const object?
- Yes. const only stops reassignment of the variable itself. obj.name = "new" is allowed, obj.newField = 1 is allowed, and array methods like push and sort work normally. Only obj = {…} throws "Assignment to constant variable".
- Why does setTimeout inside a for loop with var print the last value?
- Because var creates one binding shared by every iteration. All the callbacks close over that same variable, and by the time they run the loop has finished, so they all read the final value. Switch to let, which creates a fresh binding per iteration, and each callback keeps its own value.
- Should I use let or const by default?
- const, and switch to let only when the value must be reassigned. Starting with const means every let in the code carries information — this one moves — and it removes an entire category of accidental-reassignment bugs at no cost.
- Is var deprecated in JavaScript?
- Not formally — it works and always will, because the web cannot break old code. In practice it is obsolete: let and const cover everything it does with better scoping, and most linter configurations flag it. Treat reading it as a maintenance skill and writing it as a mistake.
- Can you redeclare a let variable?
- Not in the same scope — it is a SyntaxError thrown at parse time, before any code runs. You can declare the same name in a nested block, which shadows the outer one. var permits redeclaration in the same scope, which is how duplicate declarations go unnoticed.
- Why can't I use const in a for loop?
- A classic for loop reassigns the counter with i++, which const forbids. Use let there. In for…of and for…in each iteration creates a new binding rather than reassigning, so const is both legal and the conventional choice.
- Do let and const create properties on the window object?
- No. A top-level var in a classic script becomes a property of globalThis; let and const create bindings in the script scope that are not exposed there. In ES modules nothing is added to the global object at all, since modules have their own scope.
- What is the difference between undefined and a ReferenceError when reading a variable early?
- undefined means the binding exists and holds no value yet, which is what var gives you. A ReferenceError means either the name is in the temporal dead zone — "Cannot access before initialization" — or was never declared at all — "is not defined". The two messages distinguish the cases.
This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299