Variables — var, let, const
Deep dive into JavaScript variable declarations, scoping rules, and the Temporal Dead Zone
Last updated on
Variables are containers that store data in your program. JavaScript gives you three ways to declare them — var, let, and const — and knowing the difference is critical for interviews.
The Three Keywords
const — Your Default Choice
const API_URL = "https://api.example.com";
const user = { name: "Shiva", role: "admin" };
API_URL = "new-url"; // ❌ TypeError: Assignment to constant variable
user.name = "Alex"; // ✅ Works! The object reference didn't changeKey insight: const prevents reassignment of the variable, NOT mutation of the value. Objects and arrays declared with const can still be modified internally.
let — When You Need to Reassign
let score = 0;
score = 10; // ✅ Fine
score = 100; // ✅ Fine
let score = 5; // ❌ SyntaxError: Cannot redeclarevar — The Legacy Way (Avoid)
var count = 1;
var count = 2; // ✅ No error — this is a problem!
count = 3; // ✅ FineThe Comparison Table (Interview Must-Know)
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted? | Yes (as undefined) | Yes (TDZ) | Yes (TDZ) |
| Reassignable? | Yes | Yes | No |
| Redeclarable? | Yes | No | No |
| Added in | ES1 (1997) | ES6 (2015) | ES6 (2015) |
Scope — Where Can You Access a Variable?
Function Scope (var)
var is scoped to the nearest function, not the nearest block.
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10 ← var leaked out of the if block!
}
example();Block Scope (let and const)
let and const are scoped to the nearest { } block.
function example() {
if (true) {
let x = 10;
const y = 20;
}
console.log(x); // ❌ ReferenceError: x is not defined
console.log(y); // ❌ ReferenceError: y is not defined
}Real-World Problem — Loop with var
This is one of the most famous JavaScript interview questions:
// ❌ Bug: var is function-scoped
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (NOT 0, 1, 2)Why? var i is shared across all iterations. By the time the setTimeout callbacks run, the loop is done and i is already 3.
// ✅ Fix 1: var + IIFE (The Legacy Fix)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Output: 0, 1, 2Quick Reference: IIFE
An IIFE (Immediately Invoked Function Expression) is a function that runs the moment it is defined. By passingiinto the IIFE asj, we create a new, separate function scope for each iteration, effectively "locking in" the current value.
// ✅ Fix 2: let is block-scoped (The Modern Fix)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2Each iteration gets its own copy of i because let creates a new binding per block.
Hoisting — What Really Happens
Hoisting doesn't mean JavaScript physically cuts and moves your code to the top of the file.
Instead, the JS engine executes code in two distinct phases:
- Creation Phase: Scans the code, allocates memory for variables and functions.
- Execution Phase: Runs your code line-by-line, assigning values and calling functions.
var Hoisting
console.log(name); // undefined (not an error!)
var name = "Shiva";What the engine actually sees:
var name; // declaration hoisted & initialized to undefined
console.log(name); // undefined
name = "Shiva"; // assignment stays herelet / const Hoisting and the TDZ
console.log(age); // ❌ ReferenceError: Cannot access 'age' before initialization
let age = 25;let and const ARE hoisted, but they sit in a Temporal Dead Zone (TDZ) — the period from the start of the scope until the engine reaches the initialization line. Accessing a variable inside its TDZ throws a ReferenceError.
// TDZ starts here for 'age'
// ... any access to 'age' here throws ReferenceError
let age = 25; // TDZ ends, 'age' is now accessibleWhen to Use Which
Default to `const`
↓
Need to reassign? Use `let`
↓
Never use `var` (unless maintaining legacy code)Real-World Examples
// ✅ const for values that don't change
const MAX_RETRIES = 3;
const API_KEY = process.env.API_KEY;
const user = { name: "Shiva" }; // object ref won't change
// ✅ let for values that change
let currentPage = 1;
let isLoading = false;
let retryCount = 0;
// Loop counters
for (let i = 0; i < items.length; i++) {
// i changes each iteration
}
// Accumulating results
let total = 0;
for (const price of prices) {
total += price;
}Common Mistakes
1. Thinking const Makes Objects Immutable
const config = { debug: false };
config.debug = true; // ✅ Works — you're mutating, not reassigning
config = { debug: true }; // ❌ TypeError — this is reassignmentTo truly make an object immutable:
const config = Object.freeze({ debug: false });
config.debug = true; // Silently fails (or throws in strict mode)2. Using var in Blocks
if (true) {
var leaked = "I'm accessible outside!";
}
console.log(leaked); // "I'm accessible outside!"3. Not Understanding TDZ with typeof
// With undeclared variables, typeof is safe
console.log(typeof undeclaredVar); // "undefined" — no error
// With let/const in TDZ, typeof STILL throws
console.log(typeof myLet); // ❌ ReferenceError
let myLet = 5;Interview Questions
Q: What will this output?
var a = 1;
let b = 2;
if (true) {
var a = 10;
let b = 20;
console.log(a, b);
}
console.log(a, b);Answer: 10 20 then 10 2.
var a = 10overwrites the outera(function-scoped)let b = 20creates a NEWbinside the block (block-scoped)- Outside the block,
ais now10butbis still2
Q: Is const really constant?
Answer: The binding (the variable name → value connection) is constant. For primitives, the value can't change. For objects/arrays, the reference can't change but the contents can.