let vs const vs var: The Complete Guide to JavaScript Variables
Master the differences between let, const, and var. Learn about hoisting, scope, and when to use each declaration.
The History: Why Three Ways?
JavaScript originally only had var. It worked, but had some confusing behaviors that led to bugs. ES6 (2015) introduced let and const to fix these issues. Understanding all three is crucial because you'll encounter var in legacy code.
The Core Differences at a Glance
| Feature | var | let | const | |---------|-----|-----|-------| | Scope | Function | Block | Block | | Hoisting | Yes (undefined) | Yes (TDZ) | Yes (TDZ) | | Reassignment | Yes | Yes | No | | Redeclaration | Yes | No | No |
Let's explore what each of these means.
Scope: Where Variables Live
var: Function Scope
var is scoped to the nearest function, not the nearest block. This leads to surprising behavior:
``javascript
function example() {
if (true) {
var message = 'Hello';
}
console.log(message); // 'Hello' - still accessible!
}
// In a loop, this can cause bugs
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (not 0, 1, 2!)
`
The loop example is a classic JavaScript gotcha. By the time the setTimeout callbacks run, the loop has finished and i is 3.
let and const: Block Scope
let and const are scoped to the nearest block (anything between {}):
`javascript
function example() {
if (true) {
let message = 'Hello';
const greeting = 'Hi';
}
console.log(message); // ReferenceError!
console.log(greeting); // ReferenceError!
}
// The loop works correctly with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2 (correct!)
`
Each iteration of the loop gets its own i variable.
Hoisting: The Temporal Dead Zone
All three declarations are hoisted (moved to the top of their scope), but they behave differently.
var: Hoisted and Initialized to undefined
`javascript
console.log(name); // undefined (not an error!)
var name = 'Alice';
// JavaScript sees this as:
var name;
console.log(name); // undefined
name = 'Alice';
`
This can mask bugs because you get undefined instead of an error.
let and const: Hoisted but in the TDZ
`javascript
console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = 'Alice';
`
The "Temporal Dead Zone" (TDZ) is the period between entering the scope and the actual declaration. Accessing the variable during this time throws an error.
`javascript
// The TDZ in action
{
// TDZ starts here
console.log(typeof myVar); // undefined (var)
console.log(typeof myLet); // ReferenceError! (TDZ)
var myVar = 'var';
let myLet = 'let'; // TDZ ends here
}
`
The TDZ helps catch bugs by making it clear when you're using a variable before it's declared.
Reassignment and Redeclaration
var: Both Allowed
`javascript
var count = 1;
var count = 2; // No error - redeclaration allowed
count = 3; // No error - reassignment allowed
`
This can lead to accidental overwrites:
`javascript
var user = 'Alice';
// ... 100 lines later ...
var user = 'Bob'; // Oops! Accidentally created a new variable with same name
`
let: Reassignment Only
`javascript
let count = 1;
let count = 2; // SyntaxError: Identifier 'count' has already been declared
count = 3; // OK - reassignment allowed
`
const: Neither Allowed
`javascript
const count = 1;
const count = 2; // SyntaxError
count = 3; // TypeError: Assignment to constant variable
`
Important: const doesn't mean the value is immutable, just that the binding can't change:
`javascript
const user = { name: 'Alice' };
user.name = 'Bob'; // OK! Object properties can change
user = { name: 'Charlie' }; // TypeError! Can't reassign the variable
const numbers = [1, 2, 3];
numbers.push(4); // OK! Array methods work
numbers = [5, 6, 7]; // TypeError! Can't reassign
`
Best Practices: When to Use Each
Default to const
Use const by default. It signals that the variable won't be reassigned, making code easier to understand:
`javascript
const API_URL = 'https://api.example.com';
const user = { name: 'Alice', age: 25 };
const numbers = [1, 2, 3];
`
Use let When Reassignment is Needed
`javascript
let count = 0;
for (let i = 0; i < 10; i++) {
count += i;
}
let status = 'pending';
if (condition) {
status = 'complete';
}
`
Avoid var in Modern Code
There's almost no reason to use var in new code. The only exception might be when you intentionally want function-scoped behavior (rare).
Real-World Patterns
Configuration Objects
`javascript
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// Properties can be modified, but config can't be reassigned
config.timeout = 10000; // OK
`
Accumulator Pattern
`javascript
const items = [1, 2, 3, 4, 5];
let sum = 0; // Will be reassigned
for (const item of items) { // item is new each iteration
sum += item;
}
`
State Management
`javascript
let currentUser = null; // Will change
function login(user) { currentUser = user; }
function logout() {
currentUser = null;
}
`
Common Interview Questions
Q: What's the difference between let and const?
A: Both are block-scoped. let allows reassignment, const doesn't. Use const by default.
Q: Why does this print 3, 3, 3?
`javascript
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
`
A: var is function-scoped, so there's only one i shared across all iterations. By the time callbacks run, the loop is done and i is 3.
Q: Can you modify a const object? A: Yes! const` only prevents reassignment of the variable, not mutation of the value.
Summary
- const: Block-scoped, can't reassign. Use by default. - let: Block-scoped, can reassign. Use when value changes. - var: Function-scoped, can reassign and redeclare. Avoid in new code.
The modern JavaScript rule is simple: const first, let when needed, var never.