Let and Const
A deeper look at let and const — block scoping, the temporal dead zone, and when to use each.
Let and Const
We covered let and const in the Basics section — what they are, how to use them, and why var was replaced. This topic goes deeper — the exact rules of block scoping, the temporal dead zone, and some edge cases worth knowing.
If you need a refresher on the basics, revisit the Variables topic in the Basics section.
Block Scoping — The Core Rule
let and const are block scoped. A block is any code inside curly braces {}. The variable only exists inside the block where it was declared — and any nested blocks inside it.
{
let message = "Hello";
const PI = 3.14;
console.log(message); // Hello
console.log(PI); // 3.14
}
console.log(message); // ❌ ReferenceError
console.log(PI); // ❌ ReferenceErrorThis applies everywhere curly braces appear — if blocks, loops, functions, or just a standalone block.
Block scoping in if statements
const score = 85;
if (score >= 80) {
const grade = "A";
let feedback = "Excellent work!";
console.log(grade); // A
console.log(feedback); // Excellent work!
}
console.log(grade); // ❌ ReferenceError — only existed in the if block
console.log(feedback); // ❌ ReferenceError — only existed in the if blockBlock scoping in loops
for (let i = 0; i < 3; i++) {
const squared = i * i;
console.log(squared); // 0, 1, 4
}
console.log(i); // ❌ ReferenceError
console.log(squared); // ❌ ReferenceErrorEvery iteration of a for loop with let gets its own fresh binding — this is what makes closures inside loops work correctly.
The classic closure in a loop bug
// ❌ var — all callbacks share the same i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3
// 3
// 3
// By the time setTimeout fires, the loop is done and i is 3
// ✅ let — each iteration gets its own i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0
// 1
// 2let creates a new binding for i on every iteration. Each setTimeout callback captures its own i. This is one of the most important practical differences between var and let.
The Temporal Dead Zone — In Depth
We introduced the TDZ in the Hoisting topic. Here is the full picture.
When JavaScript scans a block before running it, it registers all let and const declarations. But they are not initialized — they sit in the Temporal Dead Zone until the line where they are declared is reached.
{
// TDZ for name starts here
console.log(name); // ❌ ReferenceError — in TDZ
let name = "Ali"; // TDZ ends here — name is now initialized
console.log(name); // ✅ Ali
}The TDZ is not about the variable not existing — it exists in memory, it is just deliberately inaccessible. This is different from var which is accessible but undefined.
console.log(typeof undeclaredVar); // "undefined" — no error for truly undeclared vars
console.log(typeof letVar); // ❌ ReferenceError — in TDZ
let letVar = "value";Even typeof — which normally never throws — throws a ReferenceError for variables in the TDZ.
TDZ in function parameters
// ❌ Using a parameter default that references a later parameter
function wrong(a = b, b = 1) {
return a + b;
}
wrong(); // ReferenceError — b is in TDZ when a's default is evaluated
// ✅ Reference earlier parameters only
function correct(a = 1, b = a) {
return a + b;
}
correct(); // 2const — What It Really Means
const prevents reassignment — not mutation. The variable cannot be pointed to a different value, but if the value is an object or array, its contents can still change.
// ✅ Primitives — truly constant
const PI = 3.14159;
const MAX_SIZE = 100;
const APP_NAME = "DocsHub";
PI = 3; // ❌ TypeError: Assignment to constant variable// Objects — reference is constant, contents are not
const user = { name: "Ali", age: 22 };
user.age = 23; // ✅ mutating contents — allowed
user.city = "Lahore"; // ✅ adding a property — allowed
user = { name: "Sara" }; // ❌ reassigning the variable — not allowed// Arrays — same rule
const scores = [88, 92, 76];
scores.push(95); // ✅ mutating contents — allowed
scores[0] = 100; // ✅ changing an item — allowed
scores = []; // ❌ reassigning — not allowedIf you want a truly immutable object, use Object.freeze():
const config = Object.freeze({
apiUrl: "https://api.example.com",
timeout: 5000
});
config.timeout = 9000; // silently fails — or throws in strict mode
console.log(config.timeout); // 5000 — unchangedObject.freeze() is shallow — nested objects are not frozen. For deep immutability you need a library or recursive freezing.
let vs const — The Decision Rule
A simple rule that works in almost every situation:
Use const by default.
Switch to let only when you know the value needs to change.// const — value never reassigned
const users = []; // array contents change but variable stays the same
const config = {}; // object properties change
const MAX_RETRIES = 3; // true constant
const apiUrl = "/api/v1"; // string constant
// let — value gets reassigned
let count = 0;
count++; // reassignment
let isLoading = false;
isLoading = true; // reassignment
let currentUser = null;
currentUser = fetchedUser; // reassignmentNotice that users and config are const even though their contents change — they are never reassigned. Only use let when the variable itself gets a new value.
Multiple Declarations and Redeclaration
// var — can be declared twice in the same scope (bug-prone)
var name = "Ali";
var name = "Sara"; // no error — silently overwrites
console.log(name); // Sara
// let — cannot be declared twice in the same scope
let name = "Ali";
let name = "Sara"; // ❌ SyntaxError: Identifier 'name' has already been declared
// const — same, cannot redeclare
const PI = 3.14;
const PI = 3; // ❌ SyntaxErrorThis is intentional — redeclaring the same variable in the same scope is almost always a bug. let and const catch it immediately.
Destructuring With let and const
Destructuring respects the same rules:
// const destructuring — neither variable can be reassigned
const { name, age } = user;
const [first, second] = scores;
// let destructuring — both variables can be reassigned
let { name, age } = user;
name = "Sara"; // ✅ allowed
// Swapping with let — only works with let, not const
let a = 1;
let b = 2;
[a, b] = [b, a]; // ✅ reassignmentSummary
letandconstare block scoped — they only exist within the{}block where they are declared- Each
forloop iteration withletgets its own binding — fixes the classic closure bug withvar - The Temporal Dead Zone —
letandconstvariables exist but are inaccessible before their declaration line - Even
typeofthrows aReferenceErrorfor TDZ variables constprevents reassignment — not mutation — objects and arrays declared withconstcan still be modified- Use
Object.freeze()for true immutability - Default rule — use
consteverywhere, switch toletonly when you need to reassign letandconstcannot be redeclared in the same scope —varcan, silently