The let Declaration
Introduction
let is the modern declaration for values that need to be reassigned. It is block-scoped, cannot be redeclared in the same scope, and has a temporal dead zone before its declaration line is executed.
In interviews, let is usually tested as the fix for var: it prevents accidental block leaks and creates a fresh binding for each for loop iteration.
Why This Matters
Most production JavaScript uses const by default and let when a value changes. Interviewers ask about let to see whether you understand lexical scope and why let loop callbacks print 0, 1, 2 while var callbacks print the final value.
Theory
Block scope
A let binding belongs to the nearest block: { ... }, if, for, while, try, or a function body. It is not visible outside that block.
| Feature | let behaviour | Why it matters |
|---|---|---|
| Scope | Block-scoped | Prevents values from leaking out of if and loop blocks. |
| Hoisting | Hoisted but uninitialised | Access before declaration throws ReferenceError. |
| Redeclaration | Not allowed in the same scope | Catches duplicate-name mistakes early. |
| Reassignment | Allowed | Use when the binding must change. |
| Loop binding | Fresh binding per iteration in for loops | Fixes the classic closure bug. |
Temporal dead zone
The temporal dead zone (TDZ) is the time from entering a scope until the let declaration is executed. The name exists, but it cannot be read or written yet. That is why typeof x can throw if x is a TDZ binding in the current scope.
Per-iteration bindings
For for (let i = 0; i < 3; i++), the language creates a new lexical binding for each iteration. Closures created inside the loop capture that iteration's i, not a single shared variable.
When to choose let
Use let only when reassignment is part of the design: counters, accumulators, state machines, retry loops, or variables assigned in different branches. If a binding never changes, prefer const.
Visual Diagrams
enter block name exists but is uninitialised <- TDZ reading name throws ReferenceError let name = 'Ada' name is usable leave block name is out of scope
`let` is known to the scope before its declaration, but it is unusable until initialised.
Code Examples
Block-scoped reassignment
The outer count and inner count are different bindings because the if block creates a lexical scope.
No redeclaration in the same scope
Trying to declare the same let name twice in one scope is a SyntaxError, which catches accidental duplicates before runtime.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1for (let i = 0; i < 3; i++) {2 setTimeout(function () {3 console.log(i);4 }, 0);5}Predict the output #2
1try {2 console.log(total);3 let total = 5;4} catch (error) {5 console.log(error.name);6}Interview Questions
1How is `let` different from `var`?
let is block-scoped, cannot be redeclared in the same scope, and is unavailable before initialisation because of the TDZ. var is function-scoped, can be redeclared, and is hoisted with undefined. In loops, let creates a fresh binding per iteration, which fixes the classic closure bug.
Follow-ups
- Is `let` hoisted?
- Why does `typeof` sometimes throw with `let`?
Quiz
1. What happens when you read a `let` variable before its declaration line runs?
Summary
- `let` is block-scoped and reassignable.
- A `let` binding is hoisted but cannot be accessed during the temporal dead zone.
- Redeclaring the same `let` name in one scope is a SyntaxError.
- `for` loops with `let` create a fresh binding per iteration, fixing the `var` closure bug.
Cheat Sheet
Use for: variables that must be reassigned.
Scope: nearest block.
TDZ: from scope entry until declaration execution; read/write throws ReferenceError.
Loop win: for (let i...) gives each iteration its own i.
Default style: prefer const; use let only when reassignment is needed.