Variables are the building blocks of every program — named containers that store data values so you can reference and manipulate them throughout your code. JavaScript has three ways to declare a variable: var, let, and const. Understanding the differences between them — scope, hoisting, and reassignment — is essential for writing predictable, bug-free JavaScript. In modern JavaScript (ES6+), let and const replace var almost entirely.
var vs let vs const
| Feature | var |
let |
const |
|---|---|---|---|
| Scope | Function scope | Block scope { } |
Block scope { } |
| Hoisting | Hoisted & initialised to undefined |
Hoisted but NOT initialised (TDZ) | Hoisted but NOT initialised (TDZ) |
| Re-declaration | Allowed in same scope | Not allowed in same scope | Not allowed in same scope |
| Reassignment | Allowed | Allowed | Not allowed |
| Global object property | Yes — window.myVar |
No | No |
| Use in modern JS | Avoid | Use for values that change | Default choice — use for everything else |
Naming Rules and Conventions
| Rule / Convention | Valid? | Example |
|---|---|---|
| Start with letter, $, or _ | ✅ Required | name, $price, _private |
| Start with a digit | ❌ Invalid | 1name — SyntaxError |
| camelCase for variables | ✅ Convention | firstName, totalPrice |
| SCREAMING_SNAKE for constants | ✅ Convention | MAX_RETRIES, API_BASE_URL |
| PascalCase for classes | ✅ Convention | UserProfile, ShoppingCart |
| Reserved keywords as names | ❌ Invalid | let, class, return — SyntaxError |
Temporal Dead Zone (TDZ)
| Scenario | var | let / const |
|---|---|---|
| Access before declaration | undefined — silently wrong |
ReferenceError — fails loudly and helpfully |
| Why TDZ is better | Bugs silently pass through | Error pinpoints the exact problem immediately |
| The TDZ period | N/A — var is always initialised | From start of block until the let/const declaration line |
const does not mean the value is immutable — it means the binding cannot be reassigned. A const array or object can still have its contents modified: const arr = [1,2,3]; arr.push(4); is perfectly valid. What is not allowed is arr = [5,6,7] — reassigning the variable itself to point to a different array.const for everything. Only switch to let when you know the value needs to change (a counter, a flag, a value updated in a loop). This mindset — reach for const first — produces cleaner code because it makes your intent explicit: “this value is not supposed to change.”var in modern JavaScript. Its function scope (not block scope) causes notoriously confusing bugs — especially inside loops and conditionals. A variable declared with var inside an if block leaks into the surrounding function, which almost never is what you want. Always use let or const.Basic Example
// ── const: value that should not change ──────────────────────────────────
const PI = 3.14159265;
const MAX_USERS = 100;
const SITE_NAME = 'StackLesson';
// ── let: value that will change ──────────────────────────────────────────
let score = 0;
let isLoggedIn = false;
let currentUser = null;
// Reassignment is fine with let
score = 42;
isLoggedIn = true;
currentUser = 'Alice';
console.log(`${currentUser}'s score: ${score}`); // Alice's score: 42
// ── Block scope demonstration ────────────────────────────────────────────
function checkAge(age) {
if (age >= 18) {
let message = 'Access granted'; // only exists inside this { }
const level = 'adult'; // same — block-scoped
console.log(message, level);
}
// console.log(message); // ReferenceError — message doesn't exist here
}
checkAge(21);
// ── var leaks out of blocks (why we avoid it) ────────────────────────────
function badExample() {
if (true) {
var leaked = 'I escaped the block!';
}
console.log(leaked); // 'I escaped the block!' — var ignores { } scope
}
badExample();
// ── const object: binding fixed, contents mutable ────────────────────────
const user = { name: 'Bob', age: 30 };
user.age = 31; // ✅ modifying property is allowed
user.email = 'b@b.com'; // ✅ adding property is allowed
// user = { name: 'Carol' }; // ❌ TypeError: Assignment to constant variable
console.log(user); // { name: 'Bob', age: 31, email: 'b@b.com' }
// ── const array: same rule ───────────────────────────────────────────────
const colours = ['red', 'green'];
colours.push('blue'); // ✅ fine — modifies array contents
console.log(colours); // ['red', 'green', 'blue']
How It Works
Step 1 — const Prevents Reassignment, Not Mutation
const user = { name: 'Bob' } locks the user variable so it always points to the same object in memory. But the object itself has no protection — you can add, remove, or change its properties freely. The const contract is about the variable binding, not the value.
Step 2 — Block Scope Means { } = New Scope
Every pair of curly braces creates a new block scope for let and const. A variable declared inside an if, for, or any { } block does not exist outside that block. This prevents accidental variable collisions and makes code much easier to reason about.
Step 3 — var’s Function Scope Causes Bugs
var leaked inside an if block is actually scoped to the surrounding function, not the if block. This means the variable is accessible outside the condition — a constant source of subtle bugs, especially in loops and async callbacks. This is why var is avoided in modern JavaScript.
Step 4 — Hoisting: var vs let/const
All variable declarations are “hoisted” — moved to the top of their scope during compilation. With var, the variable is initialised to undefined immediately, so accessing it before the declaration gives undefined silently. With let and const, the variable is hoisted but NOT initialised — accessing it before declaration throws a ReferenceError. This loud failure is actually better: it catches bugs immediately.
Step 5 — Template Literals Use Variables Cleanly
The backtick string `${currentUser}'s score: ${score}` is a template literal — it embeds variable values directly in a string using ${ } syntax. This is cleaner than string concatenation ('Hello ' + name + '!') and supports multi-line strings without escape characters.
Real-World Example: Shopping Cart Counter
// shopping-cart.js
// Constants — values that never change
const MAX_CART_ITEMS = 50;
const FREE_SHIPPING_THRESHOLD = 35.00;
const TAX_RATE = 0.08; // 8%
// State — values that change as user interacts
let cartItemCount = 0;
let cartTotal = 0.00;
let couponApplied = false;
// ── Add item to cart ──────────────────────────────────────────────────────
function addToCart(itemName, price) {
if (cartItemCount >= MAX_CART_ITEMS) {
console.warn('Cart is full — maximum items reached');
return;
}
cartItemCount++;
cartTotal += price;
const isFreeShipping = cartTotal >= FREE_SHIPPING_THRESHOLD;
const tax = cartTotal * TAX_RATE;
const grandTotal = cartTotal + (isFreeShipping ? 0 : 5.99) + tax;
console.log(`Added: ${itemName} ($${price.toFixed(2)})`);
console.log(`Cart: ${cartItemCount} items | Subtotal: $${cartTotal.toFixed(2)}`);
console.log(`Shipping: ${isFreeShipping ? 'FREE' : '$5.99'} | Total: $${grandTotal.toFixed(2)}`);
console.log('---');
}
// ── Apply coupon ──────────────────────────────────────────────────────────
function applyCoupon(code) {
const validCoupons = { 'SAVE10': 0.10, 'HALF': 0.50 };
const discount = validCoupons[code];
if (!discount) {
console.log(`Invalid coupon: ${code}`);
return;
}
if (couponApplied) {
console.log('A coupon is already applied');
return;
}
couponApplied = true;
cartTotal *= (1 - discount);
console.log(`Coupon ${code} applied! New total: $${cartTotal.toFixed(2)}`);
}
// Test it
addToCart('Wireless Mouse', 29.99);
addToCart('USB-C Cable', 9.99);
applyCoupon('SAVE10');
addToCart('Mechanical Keyboard', 89.99);
Common Mistakes
Mistake 1 — Trying to reassign a const
❌ Wrong — throws TypeError at runtime:
const maxScore = 100;
maxScore = 200; // TypeError: Assignment to constant variable
✅ Correct — use let for values that need to change:
let maxScore = 100;
maxScore = 200; // fine
Mistake 2 — Relying on var inside loops
❌ Wrong — var leaks out of the loop block:
for (var i = 0; i < 3; i++) { /* loop body */ }
console.log(i); // 3 — var leaked into function scope
✅ Correct — let stays inside the loop block:
for (let i = 0; i < 3; i++) { /* loop body */ }
console.log(i); // ReferenceError — i doesn't exist here (correct!)
Mistake 3 — Declaring without initialising and getting undefined
❌ Wrong — using a variable before it has a meaningful value:
let total;
console.log(total * 1.1); // NaN — total is undefined
✅ Correct — initialise with a sensible default:
let total = 0;
console.log(total * 1.1); // 0 — predictable
Quick Reference
| Keyword | Scope | Reassignable | Use When |
|---|---|---|---|
const |
Block | No | Default — everything that doesn’t need to change |
let |
Block | Yes | Counters, flags, values updated in loops |
var |
Function | Yes | Avoid — legacy code only |
| Template literal | — | — | `Hello ${name}!` — embed variables in strings |
| camelCase | — | — | Variables and functions: firstName, getUserData |
| SCREAMING_SNAKE | — | — | Config constants: MAX_RETRIES, API_URL |