let and const

Block-scoped declarations that replaced var. The modern way to declare variables.

Block Scope

let and const are scoped to the nearest {} block.

// Block = any code inside { }
{
  let x = 10;
  const y = 20;
  console.log(x, y); // 10, 20
}
console.log(x); // ❌ ReferenceError: x is not defined
console.log(y); // ❌ ReferenceError: y is not defined

// Blocks in if, for, while, etc:
if (true) {
  let secret = "hidden";
}
console.log(secret); // ❌ Not accessible outside

// Compared to var (function-scoped):
if (true) {
  var leaked = "visible";
}
console.log(leaked); // "visible" 😬 — var ignores blocks

let / const

Block-scoped { }

No hoisting (TDZ)

No redeclaration

var

Function-scoped

Hoisted as undefined

Can redeclare

let vs var

Why let fixed the problems with var.

// Problem 1: Loop variable leaks
for (var i = 0; i < 3; i++) {
  // ...
}
console.log(i); // 3 (leaked!)

for (let j = 0; j < 3; j++) {
  // ...
}
console.log(j); // ❌ ReferenceError (contained!)

var leaks loop variables into the outer scope. let keeps them contained in the loop block.

1 / 3

const Rules

const prevents reassignment, not mutation.

// const = cannot reassign the binding
const PI = 3.14159;
PI = 3; // ❌ TypeError: Assignment to constant variable

// Must initialize when declaring:
const x; // ❌ SyntaxError: Missing initializer

// ⚠️ Objects and arrays CAN be mutated:
const user = { name: "Alice" };
user.name = "Bob";      // ✓ OK (mutating property)
user.age = 25;          // ✓ OK (adding property)
user = { name: "Eve" }; // ❌ Cannot reassign

const nums = [1, 2, 3];
nums.push(4);     // ✓ OK (mutating array)
nums[0] = 99;     // ✓ OK (mutating element)
nums = [5, 6, 7]; // ❌ Cannot reassign

Think of const as:

"The variable name always points to the same thing" — not "the thing is frozen." Use Object.freeze() if you need true immutability.

Temporal Dead Zone

let and const exist but can't be accessed before declaration.

// The Temporal Dead Zone (TDZ):
// From block start → declaration line = TDZ

console.log(x); // ❌ ReferenceError (in TDZ)
let x = 5;      // TDZ ends here

// Compare with var:
console.log(y); // undefined (hoisted, no TDZ)
var y = 5;

// TDZ in practice:
function example() {
  // ---- TDZ for 'value' starts ----
  console.log(value); // ❌ ReferenceError
  
  if (true) {
    // ---- TDZ for 'inner' starts ----
    let inner = "hello";
    // ---- TDZ for 'inner' ends ----
  }
  
  let value = 42;
  // ---- TDZ for 'value' ends ----
  console.log(value); // 42
}
// TDZ also applies to const:
const fn = () => console.log(myConst);
// fn() here → ReferenceError (TDZ)

const myConst = "ready";
fn(); // ✓ "ready" (called after declaration)

// typeof is NOT safe in TDZ:
typeof undeclaredVar; // "undefined" (safe)
typeof myLet;        // ❌ ReferenceError
let myLet = 1;

Best Practices

Modern JavaScript convention.

// Rule: Use const by default, let when you need to reassign.
// Never use var.

// ✓ const for values that won't be reassigned:
const API_URL = "https://api.example.com";
const users = [];          // will push, but not reassign
const config = { debug: true }; // will mutate, not reassign
const handleClick = () => { /* ... */ };

// ✓ let for values that change:
let count = 0;
count++;

let current = items[0];
for (const item of items) {
  if (item.score > current.score) {
    current = item;
  }
}

// ✓ const in for...of and for...in:
for (const item of array) { /* new binding each iteration */ }
for (const key in object) { /* new binding each iteration */ }

// ✓ let in classic for loops:
for (let i = 0; i < 10; i++) { /* i changes */ }

FAQ

Common questions about let and const.