JavaScriptMedium

var vs let vs const

01

The Short Answer

var is function-scoped, hoisted with initialization to undefined, and can be redeclared. let is block-scoped, hoisted but not initialized (temporal dead zone), and cannot be redeclared. const is like let but also cannot be reassigned after initialization. In modern JavaScript, use const by default, let when you need reassignment, and avoid var entirely.

02

Scope

The most important difference is scope. var is scoped to the nearest function (or global if not in a function). let and const are scoped to the nearest block — any pair of curly braces (if, for, while, or even a standalone {}). This means var leaks out of blocks, which is a common source of bugs.

scope.tstypescript
function example() {
  if (true) {
    var varVariable = 'I leak out';
    let letVariable = 'I stay here';
    const constVariable = 'I stay here too';
  }

  console.log(varVariable);   // 'I leak out' — var is function-scoped
  console.log(letVariable);   // ReferenceError — let is block-scoped
  console.log(constVariable); // ReferenceError — const is block-scoped
}

// The classic for-loop problem
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 — var is shared across all iterations!

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100);
}
// Logs: 0, 1, 2 — let creates a new binding per iteration

The for-loop example is the classic interview question. With var, there's one shared i variable for all iterations — by the time the timeouts fire, the loop has finished and i is 3. With let, each iteration gets its own j binding, so each closure captures a different value.

03

Hoisting

All three declarations are hoisted (the engine knows about them before execution reaches the declaration line). But they differ in what happens when you access them before the declaration.

var hoisting

var declarations are hoisted AND initialized to undefined. You can access the variable before its declaration line — you just get undefined instead of an error. This is confusing and hides bugs.

var-hoisting.tstypescript
console.log(name); // undefined (not an error!)
var name = 'Alice';
console.log(name); // 'Alice'

// What the engine actually does:
// var name = undefined;  ← hoisted and initialized
// console.log(name);     ← undefined
// name = 'Alice';        ← assignment stays in place
// console.log(name);     ← 'Alice'

let/const hoisting (Temporal Dead Zone)

let and const are hoisted but NOT initialized. The period between entering the scope and reaching the declaration is called the Temporal Dead Zone (TDZ). Accessing the variable during the TDZ throws a ReferenceError. This is safer — it catches bugs where you accidentally use a variable before it's ready.

tdz.tstypescript
// Temporal Dead Zone for 'let'
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 25;

// Temporal Dead Zone for 'const'
console.log(PI); // ReferenceError: Cannot access 'PI' before initialization
const PI = 3.14159;

// The TDZ exists even within the same block:
{
  // TDZ starts here for 'x'
  console.log(x); // ReferenceError
  let x = 10;     // TDZ ends here
  console.log(x); // 10
}
04

Redeclaration

var allows redeclaring the same variable in the same scope — the second declaration is silently ignored (just becomes an assignment). let and const throw a SyntaxError if you try to redeclare in the same scope. This prevents accidental variable shadowing bugs.

redeclaration.tstypescript
// var — allows redeclaration (dangerous)
var user = 'Alice';
var user = 'Bob'; // No error — silently overwrites
console.log(user); // 'Bob'

// let — prevents redeclaration
let count = 0;
let count = 1; // SyntaxError: Identifier 'count' has already been declared

// const — same as let
const MAX = 100;
const MAX = 200; // SyntaxError

// But shadowing in a nested block is fine for let/const:
let value = 'outer';
{
  let value = 'inner'; // Different scope — this is shadowing, not redeclaration
  console.log(value);  // 'inner'
}
console.log(value); // 'outer'
05

Reassignment (const)

const prevents reassignment — you cannot point the variable at a different value after initialization. But it does NOT make the value immutable. If the value is an object or array, you can still modify its contents. const only locks the binding (the variable name), not the value it points to.

const-mutability.tstypescript
// const prevents reassignment
const name = 'Alice';
name = 'Bob'; // TypeError: Assignment to constant variable

const count = 0;
count++; // TypeError: Assignment to constant variable

// But const does NOT prevent mutation of objects/arrays!
const user = { name: 'Alice', age: 30 };
user.name = 'Bob';     // ✅ Works — mutating the object's property
user.age = 31;         // ✅ Works
user = { name: 'Eve' }; // ❌ TypeError — can't reassign the variable

const items = [1, 2, 3];
items.push(4);    // ✅ Works — mutating the array
items[0] = 99;    // ✅ Works
items = [5, 6];   // ❌ TypeError — can't reassign

// For true immutability, use Object.freeze (shallow) or structuredClone
const frozen = Object.freeze({ name: 'Alice' });
frozen.name = 'Bob'; // Silently fails (or TypeError in strict mode)

const means constant binding, not constant value

Think of const as "this variable name will always point to this thing." The thing itself can still change internally (if it's an object). For primitives, const effectively means immutable because you can't change a primitive without reassignment.

06

Full Comparison

Featurevarletconst
ScopeFunctionBlockBlock
HoistingYes + initialized to undefinedYes but TDZ (ReferenceError)Yes but TDZ (ReferenceError)
RedeclarationAllowedSyntaxErrorSyntaxError
ReassignmentAllowedAllowedTypeError
Must initialize?NoNoYes — must assign at declaration
Global object property?Yes (window.x)NoNo
07

Best Practices

Modern JavaScript conventions

  • Use `const` by default — signals the binding won't change, makes code easier to reason about
  • Use `let` only when you need to reassign (loop counters, accumulators, conditionally assigned values)
  • Never use `var` — it has no advantages over let/const and its scoping rules cause bugs
  • If a linter flags a `let` that's never reassigned, change it to `const`
08

Why Interviewers Ask This

This is a foundational JavaScript question that tests your understanding of scope, hoisting, and the temporal dead zone. Interviewers often follow up with the classic for-loop closure problem or ask you to predict output of code that mixes var and let. It reveals whether you understand JavaScript's execution model and can explain why modern code avoids var. It's also a quick signal of whether you write modern JavaScript or are stuck in pre-ES6 patterns.

Quick Revision Cheat Sheet

var: Function-scoped, hoisted + undefined, redeclarable — avoid

let: Block-scoped, TDZ, no redeclaration, reassignable

const: Block-scoped, TDZ, no redeclaration, no reassignment

const + objects: Binding is locked, but object contents can still be mutated

Default choice: const > let > var (never use var)

For-loop trap: var shares one binding across iterations; let creates one per iteration