1. Introduction
Before ES6 (2015), JavaScript only had var, which is function-scoped and gets hoisted with an initial value of undefined. This caused confusing bugs, especially inside loops and conditional blocks. ES6 introduced let and const, two new ways to declare variables that are scoped to the nearest enclosing block ({ }) rather than the nearest function. let is used for variables that will be reassigned, while const is used for bindings that should never be reassigned after their initial value is set.
Cricket analogy: Like an old scoring rule where a substitute fielder's status leaked across the whole match (function-scoped var) causing confusion, until a new rule confined a fielder's temporary role to just the current over (block-scoped let), with a permanent captain designation (const) never reassigned mid-match.
2. Syntax
// var: function-scoped, hoisted, can be redeclared
var x = 1;
// let: block-scoped, can be reassigned, cannot be redeclared in same scope
let y = 2;
y = 3;
// const: block-scoped, must be initialized, cannot be reassigned
const z = 4;
if (true) {
let blockScoped = 'inside';
const alsoBlockScoped = 'inside';
}
// blockScoped and alsoBlockScoped are NOT accessible here3. Explanation
Both let and const are confined to the block in which they are declared: an if block, a for loop body, or any pair of curly braces. This is a huge improvement over var, which leaks out of blocks into the surrounding function. A classic example is a for loop with var i: after the loop ends, i still exists in the outer scope, and closures created inside the loop all share the same i. Using let i instead gives each iteration its own fresh binding, which is exactly what most developers expect.
Cricket analogy: Like a substitute fielder's temporary assignment (var i) still lingering in the dugout roster after the over ends, so every replay reference points to the same final fielder — a fresh per-over assignment (let i) gives each over's replay its own correct fielder.
const prevents *reassignment* of the binding, not mutation of the value it points to. If const arr = [], you cannot do arr = [1, 2] (that reassigns the binding), but you absolutely can do arr.push(1) (that mutates the object the binding points to) because the array reference itself never changes. The same applies to objects: const obj = {} allows obj.name = 'Ada' but not obj = { name: 'Ada' }.
Cricket analogy: Like a permanently assigned squad number (const) that can never be reassigned to a different player, but the player wearing it can still change their batting stance freely — const arr locks the binding, not the array's contents like arr.push(1).
Gotcha — const does not freeze objects: const arr = []; arr.push(10); console.log(arr); // [10] is completely legal. To make an object truly immutable you need Object.freeze(obj), which prevents adding, removing, or changing properties (in non-strict mode it fails silently; in strict mode it throws).
Gotcha — the Temporal Dead Zone (TDZ): unlike var, let and const declarations ARE hoisted to the top of their block, but they are not initialized until the declaration line executes. Accessing them before that point throws a ReferenceError instead of returning undefined. console.log(a); let a = 5; throws ReferenceError: Cannot access 'a' before initialization, whereas the equivalent with var would silently log undefined.
4. Example
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(() => console.log('let i =', i));
}
funcs.forEach(fn => fn());
const config = { retries: 3 };
config.retries = 5; // mutation: allowed
config.timeout = 1000; // adding a property: allowed
console.log(config);
try {
config = { retries: 1 }; // reassignment: not allowed
} catch (err) {
console.log(err instanceof TypeError, err.message);
}5. Output
let i = 0
let i = 1
let i = 2
{ retries: 5, timeout: 1000 }
true Assignment to constant variable.6. Key Takeaways
- let and const are block-scoped ({ }); var is function-scoped and leaks out of blocks.
- Each loop iteration with let i creates a fresh binding, so closures capture the correct value.
- const locks the binding, not the value — arrays/objects declared with const can still be mutated.
- Reassigning a const binding throws a TypeError: Assignment to constant variable.
- let and const are hoisted but stay uninitialized in the Temporal Dead Zone until their declaration runs.
- Prefer const by default, and switch to let only when a variable genuinely needs reassignment.
Practice what you learned
1. What happens when you run `const arr = []; arr.push(1); console.log(arr);`?
2. What is the Temporal Dead Zone?
3. Which statement about var vs let inside a for-loop is correct?
4. What error does `config = { retries: 1 };` throw if `config` was declared with const?
5. How do you make an object's properties truly immutable in JavaScript?
6. What does `console.log(a); let a = 5;` produce?
Was this page helpful?
You May Also Like
Variables in JavaScript (var, let, const)
Learn how var, let, and const differ in scope, hoisting, and reassignment when declaring variables in JavaScript.
Data Types in JavaScript
Understand JavaScript's primitive and reference data types and how the typeof operator identifies them.
Closures in JavaScript
Understand how closures let inner functions remember and access variables from their outer scope, and how loop variable capture can trip you up.
for Loop in JavaScript
Master the classic for loop along with for...in and for...of, and understand the key differences between iterating indexes, keys, and values.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics