JavaScript Let
let was introduced in ES6 (2015) as a saner replacement for var. It’s block-scoped, can’t be re-declared in the same scope, and has a brief “temporal dead zone” before its declaration that catches a whole class of bugs. Reach for let whenever you have a variable that genuinely needs to change value.
The Basics
let count = 0;
count = count + 1;
console.log(count); // 1
You declare with let, you can reassign as often as you like — same as var, but with much better scoping rules.
Block Scope — The Big Win
A block is anything wrapped in { }: function bodies, if statements, for loops, even a stand-alone pair of braces. A let declared inside a block is invisible outside it.
if (true) {
let secret = "shh";
console.log(secret); // "shh"
}
console.log(secret); // ❌ ReferenceError — secret is not defined
Compare with var:
if (true) {
var leaked = "oops";
}
console.log(leaked); // "oops" — leaked out of the block!
┌────── if block ──────┐
│ │
let: │ secret exists here │ ← gone after }
│ │
└──────────────────────┘
var: │ leaked exists here │ and out here too
└──────────────────────┘──────────────────▶
Re-declaration Is Forbidden
In the same scope, you can’t declare the same let twice. This catches typos and accidental overwrites.
let user = "Ada";
let user = "Linus"; // ❌ SyntaxError: Identifier 'user' has already been declared
// You can reassign — just not re-declare
let name = "Ada";
name = "Linus"; // ✅ fine
In different scopes, the same name is fine — it’s a new variable:
let user = "Ada";
{
let user = "Linus"; // ✅ new variable inside this block
console.log(user); // "Linus"
}
console.log(user); // "Ada"
The Temporal Dead Zone (TDZ)
let is hoisted to the top of its block — but you cannot access it until the line where it’s declared. The window between block start and declaration is called the Temporal Dead Zone.
{
console.log(x); // ❌ ReferenceError — TDZ
let x = 5;
console.log(x); // 5
}
{
╔══════════════════════════╗
║ TDZ — accessing x ║ ← ReferenceError if used here
║ throws ReferenceError ║
╚══════════════════════════╝
let x = 5; ← TDZ ends here
// x is usable from this point
}
This is intentional. With var, accessing a not-yet-declared variable silently returned undefined, hiding bugs. With let, you get a loud error.
var |
let |
|
|---|---|---|
| Hoisted? | ✅ Yes | ✅ Yes |
| Initial value during hoisting | undefined |
Uninitialized (TDZ) |
| Access before declaration | Returns undefined (silent) |
Throws ReferenceError (loud) |
let in Loops — The Classic Fix
The most famous reason let exists: closures inside loops.
// ❌ The classic var loop bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 ← all three closures share ONE i
// ✅ With let, each iteration gets its own i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
Why? With let, the engine creates a fresh binding of i for each iteration of the loop. Each closure captures its own copy.
var i: ┌───┐
│ i │ ◄── all 3 callbacks point here
└───┘
let i: ┌───┐ ┌───┐ ┌───┐
│ i=0│ │ i=1│ │ i=2│ ◄── each callback has its own
└───┘ └───┘ └───┘
Tip: This single feature is the reason let won. Loops with callbacks (event listeners, timers, async work) used to require IIFE workarounds. With let, it just works.
let vs const — When to Pick Which
Both are block-scoped, both have TDZ, both forbid re-declaration. The single difference: const forbids reassignment.
const PI = 3.14;
PI = 3.15; // ❌ TypeError
let radius = 5;
radius = 10; // ✅ fine
Rule of thumb:
- Default to
const. - Use
letonly when you can point to the line that reassigns it.
| Situation | Use |
|---|---|
| API endpoint, config, formula | const |
Loop counter (for (let i…)) |
let |
| Accumulator that grows | let |
| Function parameter | (already block-scoped, no keyword) |
| Object you’ll mutate but not reassign | const |
Loop Variables: For Counter vs For Each
// Classic counter — needs let (i is reassigned each iteration)
for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}
// for…of — use const, since each iteration is a NEW binding
for (const item of items) {
console.log(item);
}
// for…in — same, each iteration is a fresh binding
for (const key of Object.keys(obj)) {
console.log(key, obj[key]);
}
Both for…of and for…in create a new binding per iteration, so const is correct — you’re not reassigning the existing one.
let at Module / Global Scope
A let at the top level of a module is module-scoped, not added to window:
// module.js
let secret = "hello";
// window.secret === undefined ✅
Compare with var at script scope, which does leak onto window:
var oldStyle = "hello";
console.log(window.oldStyle); // "hello" ← global pollution
A Realistic Example
async function paginatedFetch(url) {
const results = []; // const — never reassigned
let cursor = null; // let — gets reassigned each page
let pageNum = 0; // let — counter
do {
const params = cursor ? `?cursor=${cursor}` : "";
const res = await fetch(`${url}${params}`);
const { items, next } = await res.json();
results.push(...items);
cursor = next;
pageNum += 1;
console.log(`Fetched page ${pageNum}, ${items.length} items`);
} while (cursor);
return results;
}
Pattern: const for things that don’t change, let for the cursor and counter that genuinely evolve.
let vs var — Side by Side
| Feature | var |
let |
|---|---|---|
| Scope | Function | Block |
| Re-declarable in same scope | ✅ | ❌ |
| Hoisting behaviour | undefined |
TDZ → ReferenceError |
Adds to window at top level |
✅ | ❌ |
| Iteration captures fresh binding | ❌ | ✅ |
| Recommended? | ❌ Never | ✅ Yes |
Best Practices Checklist
- ✅ Use
letwhen (and only when) you reassign - ✅ Declare
letas close to its first use as possible - ✅ Always use
letinforcounters (for (let i = 0; …)) - ✅ Trust your linter:
prefer-constwill tell you whenletcould beconst - ❌ Don’t declare a
letat the top of a function “in case you need it later” - ❌ Don’t shadow outer
lets on purpose — it confuses readers - ❌ Don’t use
var. Ever. There is no scenario in modern code wherevaris the right answer.
Warning: Mixing var and let in the same file looks innocent but produces subtle bugs around hoisting. Pick let/const and remove every var.
What’s Next?
let is for values that change. const is for values that don’t — and it’s the keyword you’ll use 90% of the time. Let’s see how it works, what it actually locks, and how to make objects truly immutable.