Var, Let, or Const?
Understanding variable declarations is foundational to writing robust, bug-free JavaScript. Before ES6 (ECMAScript 2015), var was our only choice. Today, we have let and const and picking the wrong one can lead to silent errors, scope leaks, and unpredictable behavior.
In this guide, we will break down scope, hoisting, re-assignment rules, and the Temporal Dead Zone (TDZ) with visual mental models and interactive examples.
Quick Comparison Matrix
Before diving into details, here is the cheat sheet comparing var, let, and const:
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function Scope | Block Scope | Block Scope |
| Hoisting | Yes (initialized as undefined) | Yes (uninitialized in TDZ) | Yes (uninitialized in TDZ) |
| Re-declaration | Allowed in same scope | Forbidden | Forbidden |
| Re-assignment | Allowed | Allowed | Forbidden |
| Initialization | Optional (undefined) | Optional (undefined) | Required at declaration |
Scope: Where Do Variables Live?
Scope determines where your variables are accessible within your code execution context.
1. Function Scope (var)
Variables declared with var are scoped to the nearest enclosing function. They completely ignore block boundaries like if statements, for loops, or plain {} blocks.
function scopeExample() {
if (true) {
var message = "I am visible outside the if block!";
}
console.log(message); // "I am visible outside the if block!"
}
scopeExample();
for Loop BugBecause var ignores block scope, loop counters bleed into outer functions:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Logs 3, 3, 3!
}
2. Block Scope (let & const)
let and const respect Block Scope. A block is any code enclosed within curly braces {}.
function blockExample() {
if (true) {
let secret = "Hidden in block";
const apiKey = "12345-ABC";
}
// ReferenceError: secret is not defined
console.log(secret);
}
By switching the classic loop counter from var to let, each loop iteration gets its own fresh lexical binding:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Logs 0, 1, 2!
}
Hoisting & The Temporal Dead Zone (TDZ)
Hoisting is JavaScript's default behavior of moving variable and function declarations to the top of their containing scope during the memory allocation creation phase.
How var Hoists
When var is hoisted, it is automatically initialized with undefined:
console.log(user); // Output: undefined (No crash!)
var user = "Alex";
// How JavaScript interprets it:
// var user;
// console.log(user);
// user = "Alex";
How let and const Hoist (The Temporal Dead Zone)
let and const are also hoisted, but they remain uninitialized. The period between the start of the scope execution and the variable's declaration line is called the Temporal Dead Zone (TDZ).
Attempting to read a variable inside its TDZ triggers a runtime ReferenceError.
console.log(score); // ❌ ReferenceError: Cannot access 'score' before initialization
let score = 99;
Scope Start ──────────────────────────┐
│ Temporal Dead Zone (TDZ)
│ Accessing 'score' here throws ReferenceError
let score = 99; ──────────────────────┴ Declaration & Initialization
console.log(score); // 99 (Safe access)
Mutability: const Does Not Mean Immutable!
A common misconception is that const values are immutable (unchangeable). In reality, const only creates an immutable binding to a memory reference.
Primitives vs. Objects
- Primitive values (
number,string,boolean) assigned toconstcannot be changed. - Objects and Arrays assigned to
constcan have their properties mutated!
const user = { name: "Sarah", role: "Developer" };
// ✅ Valid: Mutating internal properties
user.role = "Lead Architect";
user.age = 28;
// ❌ Invalid: Re-assigning the reference itself
user = { name: "John" }; // TypeError: Assignment to constant variable.
To prevent mutating properties inside an object, use Object.freeze():
const config = Object.freeze({ theme: "dark" });
config.theme = "light"; // Silently fails (or throws Error in Strict Mode)
Interactive Playground
Test variable scoping and TDZ behaviors directly in your browser:
Best Practice Checklist
Follow this simple rule of thumb for clean, maintainable modern JavaScript:
- Default to
constfor all variable declarations. - Use
letonly when you know a variable value will be re-assigned (e.g., loop counters, state flags). - Avoid
varentirely in modern ES6+ codebases to eliminate scope bleeding and unexpected hoisting bugs.
Summary
varis function-scoped, hoisted asundefined, and allows re-declarations.letis block-scoped, hoisted inside the TDZ, and allows re-assignment.constis block-scoped, hoisted inside the TDZ, forbids re-assignment, but allows object property mutations.