Variables: var, let, and const

▶ Try It Yourself

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
Note: 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.
Tip: Default to 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.”
Warning: Never use 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

▶ Try It Yourself

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

🧠 Test Yourself

You have const colours = ['red', 'green']. Which operation will throw a TypeError?





▶ Try It Yourself