JavaScript interview question
What are the differences between var, let and const?
Short answer
var is function-scoped, hoisted and initialised to undefined, and can be redeclared. let and const are block-scoped and hoisted but sit in the temporal dead zone until declared. const prevents reassignment of the binding, not mutation of the object it points to. Default to const, use let when reassigning.
Answer
All three declare variables, but they differ in scope, hoisting behaviour, redeclaration and reassignment. In modern code the rule is simple: use const by default, let when the variable must be reassigned, and avoid var.
Scope
var is scoped to the enclosing function (or the global scope). let and const are scoped to the nearest block { ... }, including if, for and plain blocks.
function demo() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
}
At the top level of a classic script, var also creates a property on globalThis (window in browsers); let and const do not. In ES modules, top-level declarations of any kind are module-scoped.
Hoisting and the temporal dead zone
All declarations are hoisted to the top of their scope, but they are initialised differently:
console.log(x); // undefined (var is initialised to undefined)
var x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
The period between entering the scope and the let/const line is the temporal dead zone (TDZ). Accessing the variable there throws. That is a feature: it turns a silent undefined bug into a loud error.
Redeclaration and reassignment
var a = 1;
var a = 2; // allowed, silently overwrites
let b = 1;
// let b = 2; // SyntaxError: Identifier 'b' has already been declared
const c = 1;
// c = 2; // TypeError: Assignment to constant variable.
const also must be initialised at declaration.
const is not immutability
const freezes the binding, not the value. Objects and arrays declared with const can still be changed:
const config = { retries: 2 };
config.retries = 3; // fine
const tags = ['smoke'];
tags.push('ui'); // fine
// config = {}; // TypeError
For a shallow read-only object use Object.freeze(config) (nested objects are still mutable), or TypeScript’s readonly / as const for compile-time protection.
Why it matters in test code
The classic bug is var in a loop with async callbacks:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j)); // 0, 1, 2
}
let creates a fresh binding per iteration, so each callback closes over its own j. With var, all callbacks share one i, which is 3 by the time they run. The same issue appears when generating parameterised tests in a loop.
What interviewers look for
- Function versus block scope, with a short example.
- Hoisting explained precisely:
undefinedforvar, TDZReferenceErrorforlet/const. constmeans no reassignment, not deep immutability.- The loop-and-closure example and a clear default rule.
A strong answer covers
- var is function-scoped; let and const are block-scoped
- Hoisting: var initialised to undefined; let/const in the temporal dead zone
- const prevents reassignment but not mutation of objects
- Redeclaration allowed with var, not with let/const
- Recommends const by default, let when needed, avoid var