Three ways to declare a variable, introduced across two decades, all still valid. The difference between var, let and const comes up in almost every JavaScript interview, and more importantly it explains a whole category of bugs that otherwise look like the language behaving randomly. This article covers scope, hoisting, reassignment, the loop problem, and what const actually protects.
The short version
| var | let | const | |
|---|---|---|---|
| Scope | Function | Block | Block |
| Reassignable | Yes | Yes | No |
| Redeclarable in same scope | Yes | No | No |
| Hoisted | Yes, as undefined | Yes, but unusable | Yes, but unusable |
| Attaches to global object | Yes | No | No |
That table answers the interview question. The rest of this article explains why each row matters in code you actually write.
Scope: function versus block
A block is anything between curly braces — an if, a for, a bare { }. var ignores blocks entirely and scopes to the nearest enclosing function.
function test() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(b); // ReferenceError
}
The var declaration leaked out of the if block. This is not a bug in the example — it is how var has always worked, and it is the reason experienced JavaScript developers wrapped code in immediately-invoked functions for years just to create a private scope.
With let and const, the braces mean what they appear to mean.
Hoisting and the temporal dead zone
All three declarations are hoisted, meaning the engine registers them before executing the code. The difference is what happens if you touch them early.
console.log(x); // undefined
var x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
var is initialised to undefined at hoist time, so reading it early gives you a meaningless value and the program continues. This is worse than it sounds, because the bug surfaces somewhere else entirely — you get undefined is not a function three lines later and hunt in the wrong place.
let and const are hoisted but left uninitialised. The span between the start of the block and the declaration line is called the temporal dead zone, and accessing the variable there throws immediately. The error appears at the actual mistake, which is the whole point.
Reassignment versus redeclaration
These are different operations and the distinction matters.
let a = 1;
a = 2; // fine — reassignment
let a = 3; // SyntaxError — redeclaration
const b = 1;
b = 2; // TypeError — reassignment not allowed
var c = 1;
var c = 2; // fine, and this is the problem
The var case silently overwrites. In a three-hundred-line file, declaring var config twice produces no warning and the second one wins. With let, the engine tells you immediately.
What const actually guarantees
This is the most widely misunderstood point in the whole topic. const makes the binding constant, not the value. The variable cannot be pointed at something else. The thing it points at can still change.
const user = { name: "Ali" };
user.name = "Sara"; // allowed — mutating the object
user.age = 30; // allowed
user = { name: "Zoe" }; // TypeError — rebinding
const list = [1, 2, 3];
list.push(4); // allowed, list is now [1,2,3,4]
list = []; // TypeError
If you need the object itself to be immutable, that is a separate operation:
const settings = Object.freeze({ theme: "dark" });
settings.theme = "light"; // silently ignored, or throws in strict mode
Note that Object.freeze is shallow — nested objects remain mutable. Deep immutability requires recursion or a library.
The classic loop problem
This example has been confusing developers since closures met loops, and it is the single best demonstration of why block scope matters.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3, 3, 3
There is only one i, shared by the whole function scope. By the time the callbacks run, the loop has finished and i is 3. All three closures reference the same variable.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0, 1, 2
let creates a fresh binding for each iteration, so each closure captures its own copy. Before ES6, the workaround was to wrap the body in an immediately-invoked function to force a new scope. That entire pattern exists only to compensate for var.
Incidentally, const works in for...of and for...in loops, because a new binding is created per iteration and never reassigned. It does not work in a classic for loop, where i++ reassigns.
The global object
At the top level of a script, var attaches to the global object:
var a = 1;
let b = 2;
console.log(window.a); // 1
console.log(window.b); // undefined
This matters when multiple scripts run on the same page. A top-level var in one file can overwrite a same-named variable in another, including properties that already exist on window. let and const stay out of the global object entirely.
Which one to use
A rule most modern codebases follow: use const by default, let when you genuinely need to reassign, and var never.
The reasoning is about reading, not writing. When you encounter const, you know the binding is fixed for the rest of the block and you can stop tracking it. When you see let, that is a signal to check what changes it. In a function with twenty declarations, having two of them marked as mutable narrows your attention considerably.
Linters support this directly: prefer-const flags any let that is never reassigned, and no-var catches the rest.
Cases that still look confusing
Declaring with no value. let x; is valid and gives undefined. const x; is a syntax error, because a constant binding must be initialised at declaration.
Shadowing. An inner block can declare a variable with the same name as an outer one. This is legal and sometimes useful, but shadowing a variable in a long function usually makes the code harder to follow.
let count = 1;
{
let count = 2;
console.log(count); // 2
}
console.log(count); // 1
Assigning without declaring. Writing x = 5 with no keyword creates an implicit global in non-strict mode. In strict mode, and inside ES modules, which are strict by default, it throws. This is another reason to keep "use strict" or modules in play.
Destructuring and the same rules
Destructuring is just another way of declaring variables, so everything above applies unchanged:
const { name, email } = user;
let [first, second] = items;
const { theme = "light" } = settings; // default value
const { user: currentUser } = response; // rename
A frequent source of confusion is that a destructured const from an object is not connected to that object afterwards. Changing user.name later does not change the name variable — it captured the value at the moment of destructuring. This is fine for primitives and surprising for people expecting a live reference.
Reading older code
You will meet var constantly in tutorials, Stack Overflow answers and legacy files, and knowing how to translate is a practical skill.
In most cases a direct swap to let or const works. The cases where it does not are exactly the cases where var was doing something unintended: a variable used before its declaration line, a variable declared inside a block and read outside it, or a variable declared twice. Each of those will now throw an error, which is the tool telling you where a latent bug lived.
Do not convert a whole legacy file at once unless you have tests. Convert the function you are already changing, run it, and move on. A mass conversion that surfaces four hidden dependencies at the same time is difficult to untangle.
A quick self-test
Before reading the answer, predict what this prints:
function f() {
console.log(a);
var a = 1;
let b = 2;
{
var a = 3;
let b = 4;
console.log(a, b);
}
console.log(a, b);
}
It prints undefined, then 3 4, then 3 2. The first line works because var a is hoisted and initialised to undefined. The inner var a = 3 reassigns the same function-scoped variable rather than creating a new one, which is why the final line shows 3. The inner let b is a genuinely separate variable confined to the block, so the outer b is still 2. If you can explain all three, you understand the distinction.
Frequently asked questions
Is var deprecated?
No. JavaScript maintains backwards compatibility, so var will keep working indefinitely. It is simply no longer the right choice for new code, and you will still meet it when reading older codebases or tutorials.
Does const make my code faster?
Any difference is negligible and not a reason to choose it. The benefit is clarity and earlier errors, not performance.
Why does const work for objects I modify?
Because the variable stores a reference, and const protects the reference rather than the memory it points to. The variable still points at the same object after you change a property — only the object’s contents changed.
Should I use const for function declarations?
Both styles are common. const fn = () => {} gives you block scope and prevents accidental reassignment, but arrow functions are not hoisted, so the definition must appear before use. Traditional function declarations are fully hoisted, which some teams prefer for top-level helpers. Consistency within a codebase matters more than the choice itself.
Conclusion
The difference comes down to scope, hoisting behaviour and reassignment. var is function-scoped, hoists as undefined, allows silent redeclaration and pollutes the global object. let and const are block-scoped, throw on early access, and refuse redeclaration. Default to const, reach for let when a value genuinely changes, and remember that const freezes the binding rather than the object.