Variables in JavaScript: var, let, and const
Understand the differences between var, let, and const — including hoisting, temporal dead zone, block scope, and when to use each one.
Three Ways to Declare Variables
Variables are named containers that let your program store and reuse values. Without them you’d have to repeat every value everywhere, making code impossible to maintain. JavaScript has three keywords for declaring variables — var, let, and const — and they differ meaningfully in scope, hoisting behavior, and whether the binding can be reassigned. Choosing the right one makes your intent clear to anyone reading the code.
var name = "Alice"; // function-scoped, hoisted
let age = 30; // block-scoped, not hoisted
const PI = 3.14159; // block-scoped, not hoisted, no reassignment
Understanding the differences is one of the most important foundations in JavaScript.
var — Function Scope and Hoisting
var was the original variable declaration and is still valid JavaScript, but it comes with behavior that causes subtle bugs. The core issue is that var scopes to the enclosing function, not to the nearest block — so an if statement or a for loop does not create a new scope for var variables.
Function scope (not block scope)
function checkAge(age) {
if (age >= 18) {
var message = "Adult";
}
// message is accessible here — var ignores the if block
console.log(message); // "Adult" or undefined
}
checkAge(20); // "Adult"
checkAge(15); // undefined ← not an error, just undefined
With var, the if block creates no new scope. The variable leaks into the enclosing function.
Hoisting
JavaScript moves var declarations to the top of their function scope at compile time. Only the declaration is hoisted, not the assignment. This means you can reference a var variable before its line in the source without getting an error — you just get undefined, which is a silent bug that can be very hard to track down.
console.log(city); // undefined (not a ReferenceError!)
var city = "Tokyo";
console.log(city); // "Tokyo"
// What the engine actually sees:
var city; // declaration hoisted
console.log(city); // undefined
city = "Tokyo"; // assignment stays in place
console.log(city); // "Tokyo"
This is the classic var trap — you can use a variable before its line in the source code and get undefined instead of an error.
var in loops — a famous bug
Because var is function-scoped, every iteration of a loop shares the exact same variable. Closures inside the loop all capture the same reference, so by the time any callback runs, the loop has already finished and the variable holds its final value.
// Bug: every callback captures the same variable
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (not 0, 1, 2)
// Fix with let:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 ✓
This was one of the most common JavaScript bugs before let was introduced.
let — Block Scope
let was introduced in ES2015 and solves most of var’s problems. It scopes to the nearest enclosing block — any pair of curly braces {} — which is the intuitive behavior most programmers expect from other languages. Use let whenever you need a variable whose value will change after it is first assigned.
function getDiscount(isMember) {
if (isMember) {
let discount = 0.2;
console.log(discount); // 0.2
}
// console.log(discount); // ReferenceError: discount is not defined
}
let respects block boundaries — anything inside {} creates a new scope.
Temporal Dead Zone (TDZ)
Unlike var, accessing a let variable before its declaration throws a ReferenceError. The period from the start of the block to the let declaration is called the temporal dead zone. This is intentional — it makes the bug visible rather than silently returning undefined, turning what would have been a confusing runtime quirk into a clear error message.
console.log(score); // ReferenceError: Cannot access 'score' before initialization
let score = 100;
The period from the start of the block to the let declaration is called the temporal dead zone. This is intentional — it makes the bug visible rather than silently returning undefined.
let is reassignable
let count = 0;
count = count + 1; // fine
count++; // fine
const — Block Scope with Immutable Binding
const shares let’s block-scoping and TDZ behavior, but adds one constraint: the variable binding cannot be reassigned after initialization. This matters because most variables in well-written code are set once and never reassigned — using const signals that intent explicitly, making the code easier to follow and preventing accidental overwrites.
const MAX_RETRIES = 3;
MAX_RETRIES = 5; // TypeError: Assignment to constant variable.
const does not mean the value is immutable
This is the most common misconception about const. The keyword protects the binding — the connection between the name and the value — not the value itself. If the value is an object or array, its contents can still be changed freely.
const user = { name: "Alice", age: 30 };
// Mutating properties is allowed
user.age = 31; // ✓
user.role = "admin"; // ✓
// Reassigning the binding is not
user = { name: "Bob" }; // TypeError ✗
// For a truly immutable object, use Object.freeze:
const config = Object.freeze({ apiUrl: "https://api.example.com", timeout: 5000 });
config.timeout = 9999; // silently fails (throws in strict mode)
The same applies to arrays:
const items = [1, 2, 3];
items.push(4); // ✓ — mutates the array
items = [5, 6, 7]; // TypeError ✗ — reassigns the binding
Side-by-Side Comparison
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes (as undefined) | TDZ | TDZ |
| Reassignable | Yes | Yes | No |
| Re-declarable in same scope | Yes | No | No |
| Use in modern code | Avoid | When needed | Default |
When to Use Each
Use const by default. It signals to readers that this value doesn’t change, which reduces cognitive load. Most variables in well-written JavaScript are const.
const BASE_URL = "https://api.example.com";
const users = []; // reference doesn't change, contents may
const fetchUser = async (id) => { /* ... */ };
Use let when you genuinely need to reassign. Counters, accumulators, and loop variables are the clearest cases.
let retries = 0;
while (retries < MAX_RETRIES) {
try {
await fetchData();
break;
} catch (err) {
retries++;
}
}
Avoid var in new code. There is no modern use case where var is preferable to let or const.
Common Pitfalls
Confusing const immutability with object immutability
const settings = { theme: "dark" };
settings.theme = "light"; // works — this surprises many beginners
Re-declaring with let in the same scope
let x = 1;
let x = 2; // SyntaxError: Identifier 'x' has already been declared
This is actually a feature — it catches copy-paste bugs that var would silently allow.
Forgetting const in destructuring
// Wrong — creates implicit globals in non-strict mode
{ name, age } = getUser();
// Right — const (or let) is required
const { name, age } = getUser();
What’s Next
Now that you understand how variables are stored, the next tutorial covers JavaScript’s data types — the different kinds of values you can put inside those variables.