Variables: var, let & const
title: "Variables: var, let & const" description: "Master JavaScript variable declarations. Understand why var causes bugs, how block scope works with let and const, and how to write predictable, professional code." duration: "35 min" difficulty: "beginner" order: 2
Variables: var, let & const
Variables are named containers for data. JavaScript gives you three ways to declare them: var, let, and const. Choosing the right one is not a stylistic choice — it directly affects whether your code works correctly.
The Problem With var
var was the only way to declare variables in JavaScript before 2015. It works, but it has two behaviors that cause real bugs in production code.
1. var is Function-Scoped, Not Block-Scoped
Scope defines where a variable can be accessed. With var, the boundary is the nearest function. With let and const, the boundary is the nearest block (any pair of {}).
function checkAge(age) {
if (age >= 18) {
var message = "Access granted";
}
console.log(message); // "Access granted" — leaks out of the if block!
}
checkAge(20);
The if block does not create a new scope for var. The variable leaks into the surrounding function. This is almost never what you want.
With let, the behavior is correct:
function checkAge(age) {
if (age >= 18) {
let message = "Access granted";
}
console.log(message); // ReferenceError: message is not defined
}
The error is good here. It tells you the code has a logic problem rather than silently using a variable that was never supposed to be visible.
2. var is Hoisted and Initialized to undefined
Hoisting is JavaScript's behavior of moving variable declarations to the top of their scope before any code runs. With var, the declaration is hoisted and the variable is initialized to undefined immediately.
console.log(username); // undefined (no error, which is misleading)
var username = "alice";
console.log(username); // "alice"
JavaScript internally processes this as:
var username; // hoisted to top, set to undefined
console.log(username); // undefined
username = "alice";
console.log(username); // "alice"
You accessed a variable before you assigned it and got no error. This masks bugs that should fail loudly.
let and const: Block Scope
Introduced in ES2015, let and const both respect block scope. Their declarations are also hoisted, but they are not initialized until the line of code where you declare them. Accessing them before that line throws a ReferenceError. This window is called the Temporal Dead Zone (TDZ).
console.log(score); // ReferenceError: Cannot access 'score' before initialization
let score = 100;
The TDZ is a feature, not a bug. It prevents an entire class of subtle errors that var allows silently.
Block Scope in Practice
for (let i = 0; i < 3; i++) {
// i is scoped to this block only
}
console.log(i); // ReferenceError: i is not defined
for (var j = 0; j < 3; j++) {
// j leaks out
}
console.log(j); // 3
This matters enormously in loops that contain asynchronous callbacks — a common source of bugs when using var.
const: Your Default Choice
const declares a variable whose binding cannot be reassigned. Use it for everything that does not need to change.
const PI = 3.14159;
PI = 3; // TypeError: Assignment to constant variable
const Does Not Mean Immutable
This is the most common const misconception. const prevents you from pointing the variable at a new value. It does not freeze the contents of objects or arrays.
const user = { name: "Alice", age: 30 };
user.age = 31; // Allowed — modifying the object's property
user = { name: "Bob" }; // TypeError — reassigning the variable itself
const colors = ["red", "green"];
colors.push("blue"); // Allowed — modifying the array
colors = []; // TypeError — reassigning the variable itself
console.log(user); // { name: "Alice", age: 31 }
console.log(colors); // ["red", "green", "blue"]
The variable user is a reference to an object in memory. const locks the reference, not the object it points to. If you need a truly immutable object, use Object.freeze().
When to Use Which
Follow this rule in professional code:
- Use
constby default for everything. - Switch to
letonly when you know the value will be reassigned (loop counters, accumulators, state that changes). - Never use
varin new code.
// Good
const API_URL = "https://api.example.com";
const maxRetries = 3;
let retryCount = 0;
while (retryCount < maxRetries) {
// attempt request
retryCount++;
}
Naming Conventions
Professional JavaScript uses these conventions consistently:
// camelCase for variables and functions
const userName = "alice";
const totalPrice = 49.99;
function getUserById(id) {}
// SCREAMING_SNAKE_CASE for true constants (module-level, never change)
const MAX_CONNECTIONS = 100;
const DEFAULT_TIMEOUT_MS = 5000;
// PascalCase for classes and constructor functions
class UserProfile {}
function Person(name) {}
// Prefix booleans with is, has, can, should
const isLoggedIn = true;
const hasPermission = false;
const canEdit = true;
Descriptive names eliminate the need for comments. userAge is better than a. filteredActiveUsers is better than result.
Real-World Scope Example
Consider a shopping cart that applies a discount only for members:
const MEMBER_DISCOUNT = 0.15;
function calculateTotal(items, isMember) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
let discount = 0;
if (isMember) {
discount = subtotal * MEMBER_DISCOUNT;
}
const total = subtotal - discount;
return total;
}
const cart = [
{ name: "Keyboard", price: 79 },
{ name: "Mouse", price: 29 },
];
console.log(calculateTotal(cart, true)); // 91.4
console.log(calculateTotal(cart, false)); // 108
Each variable has the smallest scope it needs. MEMBER_DISCOUNT is module-level. subtotal, discount, and total are local to the function. Nothing leaks, nothing is accidentally overwritten.
Key Takeaways
varis function-scoped and initializes toundefinedwhen hoisted — avoid it.letandconstare block-scoped and throw aReferenceErrorif accessed before initialization (TDZ).constprevents reassignment, not mutation of objects and arrays.- Default to
const. Useletwhen the value must change. Never usevar. - Use
camelCasefor variables,SCREAMING_SNAKE_CASEfor module constants,PascalCasefor classes.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises