Quick Comparison

Propertyvarletconst
ScopeFunction (or global)Block {}Block {}
HoistingYes — initialised as undefinedYes — TDZ (not initialised)Yes — TDZ (not initialised)
ReassignmentYesYesNo
RedeclarationYes (same scope)No — SyntaxErrorNo — SyntaxError
Must initialiseNoNoYes
Use today?AvoidWhen reassignment neededDefault choice

Scope: The Most Important Difference

var is function-scoped. It ignores block boundaries like if, for, and while. This leads to subtle bugs:

var scope bug — classic problem function example() { for (var i = 0; i < 3; i++) { // do something } console.log(i); // 3 — var leaks out of the for block! } if (true) { var secret = "leaked"; } console.log(secret); // "leaked" — var escapes the if block // Classic closure bug with var: for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Prints: 3, 3, 3 — NOT 0, 1, 2 // All callbacks share the same var i, which is 3 by the time they run
let fixes the scope problem for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Prints: 0, 1, 2 — each iteration has its OWN let i if (true) { let blockScoped = "contained"; } console.log(blockScoped); // ReferenceError — let stays inside the block

Hoisting Behaviour

All three keyword types are hoisted to the top of their scope, but in different ways:

Hoisting comparison // var — hoisted and initialised as undefined: console.log(name); // undefined (no error) var name = "Alice"; console.log(name); // "Alice" // let — hoisted but NOT initialised (Temporal Dead Zone): console.log(age); // ReferenceError: Cannot access 'age' before initialisation let age = 25; // const — same as let: console.log(city); // ReferenceError const city = "Delhi"; // Function declarations ARE fully hoisted (both name AND body): greet(); // "Hello!" — works before the declaration function greet() { console.log("Hello!"); }

The Temporal Dead Zone (TDZ)

The TDZ is the window between entering a scope and the point at which a let or const declaration is reached. The variable technically exists (it was hoisted) but is not yet initialised. Any access during this period throws a ReferenceError.

Temporal Dead Zone visualised { // ---- TDZ for 'x' starts here ---- console.log(x); // ReferenceError — x is in TDZ console.log(x); // ReferenceError — still in TDZ let x = 10; // ---- TDZ ends here ---- console.log(x); // 10 — accessible now }

TDZ Is a Feature, Not a Bug

The TDZ catches a class of bugs where variables are used before they are properly set up. With var, accessing before declaration gives undefined — a silent, hard-to-debug problem. With let/const, you get an immediate, clear ReferenceError pointing exactly to the issue.

const — What It Does and Does Not Mean

Many developers think const means the value is immutable. This is incorrect. const prevents reassignment of the variable binding — not mutation of the value.

const — reassignment vs mutation const user = { name: "Alice", age: 30 }; user.name = "Bob"; // WORKS — mutating the object's property user.age = 31; // WORKS user = { name: "Charlie" }; // TypeError — cannot reassign const const nums = [1, 2, 3]; nums.push(4); // WORKS — mutating the array nums[0] = 99; // WORKS nums = [5, 6, 7]; // TypeError — cannot reassign const // For true immutability: const frozen = Object.freeze({ x: 1, y: 2 }); frozen.x = 99; // silently fails (or throws in strict mode) console.log(frozen.x); // still 1

Object.freeze() is Shallow

Object.freeze() only freezes the top-level properties. Nested objects are NOT frozen: const obj = Object.freeze({ a: { b: 1 } }); obj.a.b = 99; — this works. For deep immutability, use a library like Immer or recursively freeze all nested objects.

Redeclaration Rules

Redeclaration behaviour // var — redeclaration is SILENT (a source of bugs): var x = 1; var x = 2; // No error — silently overwrites console.log(x); // 2 // let — redeclaration throws SyntaxError: let y = 1; let y = 2; // SyntaxError: Identifier 'y' has already been declared // const — same protection: const z = 1; const z = 2; // SyntaxError // But re-declaration in a NESTED block is fine (new scope): let a = 1; { let a = 2; // OK — different block scope console.log(a); // 2 } console.log(a); // 1

Modern JavaScript Convention

The community-standard approach in modern JavaScript (and TypeScript):

  1. Use const by default — for everything. It signals "this reference does not change" and prevents accidental reassignment bugs
  2. Use let only when you need to reassign — loop counters (for let i), conditional assignments, accumulator variables
  3. Never use var — in modern projects with Babel/TypeScript transpilation, there is no reason to use var
Modern JavaScript style // Good: const by default const API_URL = 'https://api.example.com'; const user = await fetchUser(id); const { name, email } = user; // Good: let when reassignment is needed let total = 0; for (let i = 0; i < items.length; i++) { total += items[i].price; } // Or better: avoid let entirely with array methods const total = items.reduce((sum, item) => sum + item.price, 0); // Avoid: var var oldStyle = "avoid this";

How We Research and Update This Guide

We test the underlying formula or workflow, compare outputs with reliable references, and revise examples whenever the page content changes.

  • The workflow or formula is tested directly in the tool and compared against independent reference examples.
  • Examples are kept practical so readers can verify the result without hidden assumptions.
  • Pages are revised whenever the interface, calculation flow, or surrounding guidance materially changes.

Frequently Asked Questions — var vs let vs const