We are still cooking the magic in the way!
Variables: var, let, and const
What Are Variables?
A variable is a named container that stores a value in your program's memory. Think of a variable as a labeled box: you give it a name (the label), and you can put a value inside it (the contents). Later, you can retrieve the value by referring to the variable's name, update it with a new value, or use it in calculations and operations.
Variables are fundamental to every programming language. Without them, you would have to hard-code every value directly into your program, making it impossible to build anything dynamic or interactive. Variables allow your programs to remember information, respond to user input, and change behavior based on conditions.
In JavaScript, you have three keywords for declaring variables: var, let, and const. Each has different rules about scope, reassignment, and hoisting. Understanding the differences between them is crucial for writing clean, bug-free JavaScript code.
Example: Variables as Named Containers
// Declaring variables and assigning values
let userName = 'Alice'; // A box labeled "userName" containing "Alice"
let userAge = 30; // A box labeled "userAge" containing 30
let isLoggedIn = true; // A box labeled "isLoggedIn" containing true
// Using variables
console.log(userName); // Output: Alice
console.log(userAge); // Output: 30
console.log(isLoggedIn); // Output: true
// Using variables in expressions
console.log('Hello, ' + userName + '! You are ' + userAge + ' years old.');
// Output: Hello, Alice! You are 30 years old.
Declaring Variables with var
The var keyword was the original and only way to declare variables in JavaScript from 1995 until 2015. While var still works in modern JavaScript, it has several quirks and pitfalls that make it problematic. Understanding var is important because you will encounter it in older codebases, but you should avoid using it in new code.
Example: Declaring Variables with var
// Basic var declaration
var greeting = 'Hello, World!';
console.log(greeting); // Output: Hello, World!
// Reassigning a var variable
var score = 0;
score = 10;
score = score + 5;
console.log(score); // Output: 15
// Declaring without assigning (value is undefined)
var result;
console.log(result); // Output: undefined
result = 42;
console.log(result); // Output: 42
// Multiple declarations on one line (not recommended for readability)
var a = 1, b = 2, c = 3;
console.log(a, b, c); // Output: 1 2 3
Function Scope of var
The most important thing to understand about var is its function scope. A variable declared with var is accessible anywhere within the function it was declared in, regardless of blocks like if statements, for loops, or while loops. If declared outside any function, it becomes a global variable accessible everywhere.
Example: var Is Function-Scoped, NOT Block-Scoped
function demonstrateVarScope() {
var x = 10;
if (true) {
var y = 20; // This is NOT confined to the if block!
var x = 30; // This OVERWRITES the x above!
console.log(x); // Output: 30
console.log(y); // Output: 20
}
// y is accessible here because var is function-scoped
console.log(x); // Output: 30 (overwritten!)
console.log(y); // Output: 20 (leaked out of the if block!)
}
demonstrateVarScope();
// Compare this with a for loop
function loopExample() {
for (var i = 0; i < 5; i++) {
// i is accessible inside the loop
}
// i is STILL accessible here! It did not stay inside the for block.
console.log(i); // Output: 5
}
loopExample();
var is one of the most common sources of bugs in JavaScript. Variables declared with var inside if blocks, for loops, or while loops "leak" out of those blocks and are accessible in the entire function. This can lead to accidental variable overwrites and hard-to-find bugs. This is the primary reason let and const were introduced in ES6.Hoisting of var
Hoisting is a JavaScript mechanism where variable and function declarations are moved to the top of their scope before the code executes. With var, the declaration is hoisted but the assignment stays in place. This means you can reference a var variable before it is declared in the code without getting a ReferenceError -- but its value will be undefined.
Example: Hoisting with var
// What you write:
console.log(message); // Output: undefined (NOT an error!)
var message = 'Hello!';
console.log(message); // Output: Hello!
// What the JavaScript engine actually sees (after hoisting):
// var message; // Declaration is hoisted to the top
// console.log(message); // undefined (declared but not yet assigned)
// message = 'Hello!'; // Assignment stays in place
// console.log(message); // Hello!
// Another example
console.log(count); // Output: undefined
var count = 5;
console.log(count); // Output: 5
// This is confusing and error-prone!
// With let or const, this would throw a ReferenceError instead,
// which is a much clearer signal that something is wrong.
var Allows Redeclaration
Another quirk of var is that it allows you to declare the same variable name multiple times in the same scope without any error. This can easily lead to accidental overwrites.
Example: Redeclaring var Variables
var name = 'Alice';
console.log(name); // Output: Alice
var name = 'Bob'; // No error! Silently overwrites the previous value.
console.log(name); // Output: Bob
// In a large file with hundreds of lines, you might accidentally
// redeclare a variable without realizing it, causing subtle bugs.
// This is another reason to prefer let and const.
Declaring Variables with let
The let keyword was introduced in ES6 (2015) to fix the problems with var. It provides block-level scoping, which means a variable declared with let is only accessible within the block (denoted by curly braces {}) where it was declared. This is more intuitive and prevents the scope-leaking bugs that plague var.
Example: Declaring Variables with let
// Basic let declaration
let greeting = 'Hello, World!';
console.log(greeting); // Output: Hello, World!
// Reassigning a let variable
let score = 0;
score = 10;
score = score + 5;
console.log(score); // Output: 15
// Declaring without assigning
let result;
console.log(result); // Output: undefined
result = 42;
console.log(result); // Output: 42
Block Scope of let
Block scope means that a variable declared with let inside a block (an if statement, a for loop, a while loop, or any code between {}) is only accessible inside that block. Once execution leaves the block, the variable no longer exists.
Example: let Is Block-Scoped
function demonstrateLetScope() {
let x = 10;
if (true) {
let y = 20; // y is confined to this if block
let x = 30; // This is a NEW variable, separate from the outer x
console.log(x); // Output: 30 (inner x)
console.log(y); // Output: 20
}
console.log(x); // Output: 10 (outer x is unchanged!)
// console.log(y); // ERROR: y is not defined (it stayed in the if block)
}
demonstrateLetScope();
// The for loop variable stays inside the loop
function loopExample() {
for (let i = 0; i < 5; i++) {
console.log(i); // Outputs: 0, 1, 2, 3, 4
}
// console.log(i); // ERROR: i is not defined
}
loopExample();
let inside a block, you immediately know its lifetime -- it exists only within those curly braces. This makes your code easier to understand and debug.Temporal Dead Zone (TDZ)
Unlike var, which is hoisted and initialized with undefined, variables declared with let are hoisted but not initialized. The time between entering the scope and the actual declaration is called the Temporal Dead Zone (TDZ). Accessing a let variable in the TDZ throws a ReferenceError.
Example: Temporal Dead Zone with let
// The TDZ starts at the beginning of the block
// and ends at the let declaration
{
// TDZ starts here for the variable 'message'
// console.log(message); // ERROR: Cannot access 'message' before initialization
// console.log(message); // ERROR: Still in the TDZ
let message = 'Hello!'; // TDZ ends here
console.log(message); // Output: Hello! (works fine)
}
// Compare with var behavior
{
console.log(greeting); // Output: undefined (var is hoisted and initialized)
var greeting = 'Hi!';
console.log(greeting); // Output: Hi!
}
// Real-world example of TDZ catching a bug
function processOrder(items) {
// console.log(total); // ERROR: Cannot access 'total' before initialization
// This error actually HELPS you -- it tells you that you are
// trying to use a variable before it has been assigned a value.
let total = 0;
for (let item of items) {
total += item.price;
}
return total;
}
var, you would silently get undefined, which could lead to subtle bugs that are much harder to track down.let Does Not Allow Redeclaration
Unlike var, you cannot declare the same variable name twice in the same scope with let. This prevents accidental overwrites.
Example: No Redeclaration with let
let name = 'Alice';
// let name = 'Bob'; // ERROR: Identifier 'name' has already been declared
// However, you CAN have the same name in different scopes
let color = 'red';
if (true) {
let color = 'blue'; // This is fine -- different scope (inner block)
console.log(color); // Output: blue
}
console.log(color); // Output: red (outer variable is unchanged)
Declaring Variables with const
The const keyword, also introduced in ES6, declares a constant -- a variable whose binding cannot be reassigned after its initial assignment. Like let, const is block-scoped and has a Temporal Dead Zone. The key difference is that const requires an initial value at the time of declaration and cannot be reassigned.
Example: Declaring Constants with const
// const requires initialization at declaration
const PI = 3.14159;
const APP_NAME = 'My Application';
const MAX_USERS = 100;
const IS_PRODUCTION = false;
console.log(PI); // Output: 3.14159
console.log(APP_NAME); // Output: My Application
// const CANNOT be reassigned
// PI = 3.14; // ERROR: Assignment to constant variable
// APP_NAME = 'New Name'; // ERROR: Assignment to constant variable
// const MUST be initialized at declaration
// const x; // ERROR: Missing initializer in const declaration
const with Objects and Arrays: Reassignment vs Mutation
This is one of the most commonly misunderstood aspects of const. When you declare an object or array with const, the binding (the variable name pointing to the value) cannot change, but the contents of the object or array CAN be modified. This is the critical difference between reassignment and mutation.
Example: const with Objects -- Reassignment vs Mutation
// const prevents REASSIGNMENT (changing what the variable points to)
const user = { name: 'Alice', age: 30 };
// You CANNOT reassign the variable to a new object
// user = { name: 'Bob', age: 25 }; // ERROR: Assignment to constant variable
// user = 'something else'; // ERROR: Assignment to constant variable
// But you CAN MUTATE the object (change its properties)
user.name = 'Bob'; // This works!
user.age = 25; // This works!
user.email = 'bob@test.com'; // Adding new properties works!
delete user.age; // Deleting properties works!
console.log(user);
// Output: { name: 'Bob', email: 'bob@test.com' }
// Think of it this way:
// const means the "box label" cannot be moved to a different box
// But you can still change what is INSIDE the box
Example: const with Arrays -- Reassignment vs Mutation
const fruits = ['apple', 'banana', 'cherry'];
// You CANNOT reassign the array
// fruits = ['grape', 'melon']; // ERROR: Assignment to constant variable
// But you CAN mutate the array (change its contents)
fruits.push('date'); // Add to the end
fruits.pop(); // Remove from the end
fruits[0] = 'avocado'; // Change an element
fruits.splice(1, 1); // Remove an element
console.log(fruits);
// Output: ['avocado', 'cherry']
// Common array methods that MUTATE the array all work with const:
const numbers = [3, 1, 4, 1, 5];
numbers.sort(); // Sorts in place
numbers.reverse(); // Reverses in place
console.log(numbers); // Output: [5, 4, 3, 1, 1]
const makes a value completely immutable (unchangeable). This is NOT true for objects and arrays. const only prevents reassignment of the variable binding -- it does not freeze the value. If you truly need an immutable object, use Object.freeze(), but be aware that it only performs a shallow freeze (nested objects are not frozen).Example: Object.freeze() for True Immutability
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
});
// Now mutations are silently ignored (or throw errors in strict mode)
config.apiUrl = 'https://other.com'; // Silently fails
config.newProp = 'test'; // Silently fails
delete config.timeout; // Silently fails
console.log(config);
// Output: { apiUrl: 'https://api.example.com', timeout: 5000, retries: 3 }
// With strict mode:
'use strict';
// config.apiUrl = 'https://other.com'; // ERROR: Cannot assign to read-only property
Block Scope vs Function Scope: A Complete Comparison
Understanding the difference between block scope and function scope is essential. Let us put all three keywords side by side in a comprehensive comparison.
Example: Scope Comparison -- var vs let vs const
function scopeComparison() {
// All three are accessible within the function
var varVariable = 'I am var';
let letVariable = 'I am let';
const constVariable = 'I am const';
if (true) {
var varInBlock = 'var in block';
let letInBlock = 'let in block';
const constInBlock = 'const in block';
console.log(varInBlock); // Output: var in block
console.log(letInBlock); // Output: let in block
console.log(constInBlock); // Output: const in block
}
// After the block:
console.log(varInBlock); // Output: var in block (leaked out!)
// console.log(letInBlock); // ERROR: letInBlock is not defined
// console.log(constInBlock); // ERROR: constInBlock is not defined
}
scopeComparison();
// Global scope behavior
var globalVar = 'I am global var';
let globalLet = 'I am global let';
const globalConst = 'I am global const';
// var attaches to the window object in browsers
// console.log(window.globalVar); // 'I am global var'
// console.log(window.globalLet); // undefined
// console.log(window.globalConst); // undefined
Example: The Classic for Loop Problem with var
// THE CLASSIC BUG: var in a for loop with setTimeout
console.log('--- Using var ---');
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log('var i:', i);
}, 100);
}
// Output (after 100ms):
// var i: 3
// var i: 3
// var i: 3
// WHY? Because var is function-scoped, there is only ONE 'i' variable.
// By the time setTimeout runs, the loop has finished and i is 3.
// THE FIX: let in a for loop with setTimeout
console.log('--- Using let ---');
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log('let j:', j);
}, 100);
}
// Output (after 100ms):
// let j: 0
// let j: 1
// let j: 2
// WHY? Because let is block-scoped, each iteration of the loop
// creates a NEW 'j' variable with its own value.
setTimeout for loop example is one of the most famous JavaScript interview questions. It demonstrates exactly why let was created -- to provide predictable block scoping that prevents this class of bugs entirely.Variable Naming Conventions
Choosing good variable names is one of the most important skills in programming. Clear, descriptive names make your code self-documenting and reduce the need for comments.
Rules (What JavaScript Allows)
- Names can contain letters, digits, underscores (
_), and dollar signs ($). - Names must begin with a letter, underscore, or dollar sign (NOT a digit).
- Names are case-sensitive (
myVarandmyvarare different). - Reserved words cannot be used as variable names (like
let,const,class,return,if, etc.).
Conventions (What Developers Agree On)
- camelCase for regular variables and functions:
firstName,userAge,calculateTotal,isLoggedIn. - UPPER_SNAKE_CASE for constants that represent fixed, configuration-like values:
MAX_RETRIES,API_BASE_URL,DEFAULT_TIMEOUT. - PascalCase for classes and constructor functions:
UserProfile,ShoppingCart,HttpClient. - Use descriptive names that reveal intent. Prefer
userAgeoveraorx. - Boolean variables should read like questions:
isActive,hasPermission,canEdit,shouldRetry.
Example: Good vs Bad Variable Names
// BAD: Meaningless or abbreviated names
let x = 'Alice';
let n = 30;
let f = true;
let arr = ['apple', 'banana'];
let temp = 99.5;
let d = new Date();
// GOOD: Descriptive names that explain what the value represents
let userName = 'Alice';
let userAge = 30;
let isSubscribed = true;
let shoppingCartItems = ['apple', 'banana'];
let temperatureInFahrenheit = 99.5;
let currentDate = new Date();
// BAD: Too long or redundant
let theUserFirstNameString = 'Alice';
let arrayOfAllShoppingCartItemsThatUserHasAdded = [];
// GOOD: Concise but clear
let firstName = 'Alice';
let cartItems = [];
// Constants: use UPPER_SNAKE_CASE for fixed values
const MAX_LOGIN_ATTEMPTS = 5;
const API_BASE_URL = 'https://api.example.com';
const DEFAULT_LANGUAGE = 'en';
// But use camelCase for const variables that are just not reassigned
const userName2 = getUserInput(); // Determined at runtime
const filteredList = items.filter(i => i.active); // Not a fixed constant
PI, MAX_SIZE, API_KEY). For const variables that hold values determined at runtime (like the result of a function call or a DOM query), use regular camelCase. The distinction is about the nature of the value, not the const keyword.When to Use var, let, or const
Now that you understand the differences, here is the modern best practice for choosing between the three keywords:
The Modern Rule
- Use
constby default. Most variables in well-written code do not need to be reassigned. Starting withconstmakes your intentions clear: this value should not change. - Use
letwhen you need to reassign. Loop counters, accumulators, values that change based on conditions -- these are valid use cases forlet. - Never use
var. There is no scenario in modern JavaScript wherevaris the better choice. It exists only for backward compatibility with old code.
Example: Choosing the Right Keyword
// Use const: values that should not be reassigned
const API_URL = 'https://api.example.com';
const user = { name: 'Alice', role: 'admin' };
const colors = ['red', 'green', 'blue'];
const element = document.getElementById('title');
const isProduction = process.env.NODE_ENV === 'production';
// Use let: values that NEED to be reassigned
let score = 0;
score += 10;
let currentPage = 1;
currentPage++;
let status = 'pending';
if (isApproved) {
status = 'approved';
}
let total = 0;
for (const item of cartItems) {
total += item.price; // total changes with each iteration
}
// Loop counters
for (let i = 0; i < 10; i++) {
console.log(i);
}
// NEVER use var
// var name = 'Alice'; // Don't do this
// let name = 'Alice'; // Do this instead (if you need to reassign)
// const name = 'Alice'; // Or this (if the value won't change)
Hoisting: A Complete Overview
Let us consolidate everything about hoisting with all three declaration types side by side.
Example: Hoisting Behavior Comparison
// ===== var: Hoisted and initialized with undefined =====
console.log(varMessage); // Output: undefined (no error)
var varMessage = 'Hello from var';
console.log(varMessage); // Output: Hello from var
// ===== let: Hoisted but NOT initialized (TDZ) =====
// console.log(letMessage); // ERROR: Cannot access 'letMessage' before initialization
let letMessage = 'Hello from let';
console.log(letMessage); // Output: Hello from let
// ===== const: Hoisted but NOT initialized (TDZ) =====
// console.log(constMessage); // ERROR: Cannot access 'constMessage' before initialization
const constMessage = 'Hello from const';
console.log(constMessage); // Output: Hello from const
// ===== Function declarations: Fully hoisted =====
sayHello(); // Output: Hello! (works even before the declaration)
function sayHello() {
console.log('Hello!');
}
// ===== Function expressions with var: Partially hoisted =====
// greet(); // ERROR: greet is not a function (it is undefined)
var greet = function() {
console.log('Hi!');
};
greet(); // Output: Hi!
Practical Real-World Examples
Let us look at practical scenarios where understanding variables really matters.
Example: Shopping Cart
// Product prices (const: these are fixed values)
const TAX_RATE = 0.08;
const SHIPPING_THRESHOLD = 50;
const SHIPPING_COST = 5.99;
// Cart items (const: we won't reassign the array, but we will modify its contents)
const cart = [];
// Add items to the cart
cart.push({ name: 'T-Shirt', price: 25.99, quantity: 2 });
cart.push({ name: 'Jeans', price: 49.99, quantity: 1 });
cart.push({ name: 'Sneakers', price: 89.99, quantity: 1 });
// Calculate totals (let: these values change during calculation)
let subtotal = 0;
for (const item of cart) {
subtotal += item.price * item.quantity;
}
let tax = subtotal * TAX_RATE;
let shipping = subtotal >= SHIPPING_THRESHOLD ? 0 : SHIPPING_COST;
let grandTotal = subtotal + tax + shipping;
console.log('Subtotal: $' + subtotal.toFixed(2));
console.log('Tax: $' + tax.toFixed(2));
console.log('Shipping: $' + (shipping === 0 ? 'Free' : shipping.toFixed(2)));
console.log('Total: $' + grandTotal.toFixed(2));
Example: User Authentication Flow
// Configuration (const: fixed values)
const MAX_LOGIN_ATTEMPTS = 3;
const LOCKOUT_DURATION_MINUTES = 15;
// State tracking (let: these change during the flow)
let loginAttempts = 0;
let isLocked = false;
let lastAttemptTime = null;
function attemptLogin(username, password) {
if (isLocked) {
const minutesSinceLock = (Date.now() - lastAttemptTime) / 60000;
if (minutesSinceLock < LOCKOUT_DURATION_MINUTES) {
const remainingMinutes = Math.ceil(LOCKOUT_DURATION_MINUTES - minutesSinceLock);
console.log('Account locked. Try again in ' + remainingMinutes + ' minutes.');
return false;
}
// Lockout period has passed, reset
isLocked = false;
loginAttempts = 0;
}
// Simulate credential check
const isValid = username === 'admin' && password === 'secret123';
if (isValid) {
loginAttempts = 0;
console.log('Login successful! Welcome, ' + username + '.');
return true;
}
loginAttempts++;
lastAttemptTime = Date.now();
if (loginAttempts >= MAX_LOGIN_ATTEMPTS) {
isLocked = true;
console.log('Too many failed attempts. Account locked for ' + LOCKOUT_DURATION_MINUTES + ' minutes.');
} else {
const remaining = MAX_LOGIN_ATTEMPTS - loginAttempts;
console.log('Invalid credentials. ' + remaining + ' attempts remaining.');
}
return false;
}
Example: Temperature Converter
// Conversion formulas are constants
const FAHRENHEIT_TO_CELSIUS_OFFSET = 32;
const FAHRENHEIT_TO_CELSIUS_RATIO = 5 / 9;
function convertTemperature(value, fromUnit) {
// result will be reassigned based on the conversion direction
let result;
let toUnit;
if (fromUnit === 'C') {
result = (value * (1 / FAHRENHEIT_TO_CELSIUS_RATIO)) + FAHRENHEIT_TO_CELSIUS_OFFSET;
toUnit = 'F';
} else if (fromUnit === 'F') {
result = (value - FAHRENHEIT_TO_CELSIUS_OFFSET) * FAHRENHEIT_TO_CELSIUS_RATIO;
toUnit = 'C';
} else {
console.error('Unknown unit: ' + fromUnit);
return;
}
// Round to 2 decimal places
const rounded = Math.round(result * 100) / 100;
console.log(value + ' degrees ' + fromUnit + ' = ' + rounded + ' degrees ' + toUnit);
return rounded;
}
convertTemperature(100, 'C'); // 100°C = 212°F
convertTemperature(32, 'F'); // 32°F = 0°C
convertTemperature(72, 'F'); // 72°F = 22.22°C
Summary: var vs let vs const
Here is a complete reference table summarizing the differences between all three variable declaration keywords:
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes (initialized as undefined) | Yes (TDZ, not initialized) | Yes (TDZ, not initialized) |
| Reassignment | Allowed | Allowed | Not Allowed |
| Redeclaration | Allowed | Not Allowed | Not Allowed |
| Must Initialize | No | No | Yes |
| Recommended | Never | When reassignment needed | Default choice |
Practice Exercise 1: Scope Detective
Without running the code, predict the output of each console.log() statement in the following program. Then run it in your browser console to verify your answers:
var a = 1; { var a = 2; } console.log(a);let b = 1; { let b = 2; } console.log(b);const c = 1; { const c = 2; console.log(c); } console.log(c);for (var i = 0; i < 3; i++) {} console.log(i);for (let j = 0; j < 3; j++) {} console.log(typeof j);
The answers are: (1) 2, (2) 1, (3) 2 then 1, (4) 3, (5) "undefined". If you got any wrong, go back and re-read the section on block scope vs function scope.
Practice Exercise 2: Refactor var to let and const
Take the following code that uses only var and refactor it to use let and const appropriately. Decide which variables should be const (not reassigned) and which should be let (reassigned). Create a file called refactor.js and test it in your browser.
var taxRate = 0.10;
var items = [12.99, 24.50, 8.75, 15.00];
var total = 0;
for (var i = 0; i < items.length; i++) {
var price = items[i];
total = total + price;
}
var tax = total * taxRate;
var grandTotal = total + tax;
console.log('Total: $' + grandTotal.toFixed(2));
Practice Exercise 3: Build a Counter
Create an HTML page with a number displayed on screen (starting at 0), an "Increment" button, a "Decrement" button, and a "Reset" button. Use JavaScript to make the buttons work. Think carefully about which variables should be const and which should be let. The counter value will change, so it should be let. The DOM element references and button references will not be reassigned, so they should be const. Add a rule that the counter cannot go below 0 (show a warning in the console if the user tries).
Practice Exercise 4: Variable Naming Challenge
Rename the following poorly-named variables to follow JavaScript naming conventions. For each one, decide whether it should be const or let:
var x = 'John Smith';
var n = 42;
var flag = true;
var arr = ['Reading', 'Coding', 'Gaming'];
var TEMP = 98.6;
var d = new Date();
var e = document.getElementById('main');
var s = 0; // used to track score in a game
Write out your renamed versions with proper const or let keywords. Example: const fullName = 'John Smith';