JavaScript Variables: var, let, and const Explained
JavaScript has evolved significantly since its creation in 1995. For nearly two decades, `var` was the only way to declare variables. Then ES6 (ES2015) introduced `let` and `const`, fundamentally...
Key Insights
varuses function scope and hoisting, which leads to confusing bugs—avoid it in modern JavaScriptconstdoesn’t make values immutable; it prevents reassignment of the binding, meaning objects and arrays can still be modified- Default to
constfor everything, only useletwhen you need reassignment, and pretendvardoesn’t exist
Introduction: Why Variable Declarations Matter
JavaScript has evolved significantly since its creation in 1995. For nearly two decades, var was the only way to declare variables. Then ES6 (ES2015) introduced let and const, fundamentally changing how we write JavaScript.
Understanding the differences between these three keywords isn’t academic trivia—it directly impacts code quality, debugging difficulty, and the types of bugs you’ll encounter. Scope and mutability issues are among the most common sources of JavaScript bugs, especially for developers transitioning from other languages.
Here’s a quick look at all three in action:
var oldSchool = 'I am function-scoped';
let modernVariable = 'I am block-scoped';
const CONSTANT_BINDING = 'I am also block-scoped, but cannot be reassigned';
oldSchool = 'Changed'; // Works
modernVariable = 'Changed'; // Works
CONSTANT_BINDING = 'Changed'; // TypeError: Assignment to constant variable
var: The Legacy Declaration
The var keyword uses function scope, not block scope. This means a variable declared with var is available throughout the entire function, regardless of where within that function it was declared.
function demonstrateVarScope() {
if (true) {
var message = 'Hello';
}
console.log(message); // 'Hello' - var leaks out of the if block
}
function demonstrateVarInLoop() {
for (var i = 0; i < 3; i++) {
// loop body
}
console.log(i); // 3 - i is accessible outside the loop
}
This behavior is counterintuitive if you’re coming from languages like Java, C++, or Python where block scope is the norm.
Hoisting makes var even more confusing. JavaScript “hoists” variable declarations to the top of their scope, but not their assignments:
function demonstrateHoisting() {
console.log(myVar); // undefined (not ReferenceError!)
var myVar = 'Hello';
console.log(myVar); // 'Hello'
}
// This is what JavaScript effectively does:
function demonstrateHoisting() {
var myVar; // Declaration hoisted
console.log(myVar); // undefined
myVar = 'Hello'; // Assignment stays in place
console.log(myVar);
}
You can also re-declare the same variable multiple times with var, which silently overwrites previous declarations:
var user = 'Alice';
var user = 'Bob'; // No error, just overwrites
console.log(user); // 'Bob'
These characteristics make var a liability in modern codebases. The lack of block scope creates unexpected behavior, hoisting enables using variables before they’re defined, and silent re-declaration hides potential bugs.
let: Block-Scoped Variables
The let keyword introduced proper block scope to JavaScript. A block is any code between curly braces: if statements, loops, functions, or standalone blocks.
function demonstrateLetScope() {
if (true) {
let message = 'Hello';
console.log(message); // 'Hello'
}
console.log(message); // ReferenceError: message is not defined
}
for (let i = 0; i < 3; i++) {
// i is scoped to this loop
}
console.log(i); // ReferenceError: i is not defined
While let declarations are also hoisted, they exist in a “Temporal Dead Zone” (TDZ) from the start of the block until the declaration is reached. Accessing a let variable in the TDZ throws a ReferenceError:
function demonstrateTDZ() {
console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = 'Hello';
}
This is actually better than var’s behavior because it catches bugs immediately rather than returning undefined.
You cannot re-declare a let variable in the same scope:
let user = 'Alice';
let user = 'Bob'; // SyntaxError: Identifier 'user' has already been declared
However, you can reassign it:
let counter = 0;
counter = 1; // Perfectly fine
counter += 1; // Also fine
Use let when you need a variable that will be reassigned: loop counters, accumulators, state that changes, or any value that needs to be updated.
function calculateSum(numbers) {
let sum = 0; // Will be reassigned in the loop
for (let num of numbers) {
sum += num;
}
return sum;
}
const: Immutable Bindings
The const keyword creates a constant binding—not a constant value. This distinction is crucial and frequently misunderstood.
With primitive values (strings, numbers, booleans), const behaves as you’d expect:
const PI = 3.14159;
PI = 3.14; // TypeError: Assignment to constant variable
const name = 'Alice';
name = 'Bob'; // TypeError: Assignment to constant variable
But with objects and arrays, const only prevents reassignment of the variable itself. The contents can still be modified:
const user = { name: 'Alice', age: 30 };
user.age = 31; // This works fine
user.email = 'alice@example.com'; // This also works
const numbers = [1, 2, 3];
numbers.push(4); // Works
numbers[0] = 99; // Works
user = { name: 'Bob' }; // TypeError: Assignment to constant variable
numbers = []; // TypeError: Assignment to constant variable
Like let, const is block-scoped and has a Temporal Dead Zone:
{
console.log(myConst); // ReferenceError
const myConst = 'Hello';
}
console.log(myConst); // ReferenceError: out of scope
You must initialize a const variable when you declare it:
const name; // SyntaxError: Missing initializer in const declaration
const name = 'Alice'; // Correct
If you truly need immutable objects or arrays, use Object.freeze():
const config = Object.freeze({ apiUrl: 'https://api.example.com' });
config.apiUrl = 'https://different.com'; // Silently fails (throws in strict mode)
console.log(config.apiUrl); // Still 'https://api.example.com'
Practical Guidelines: When to Use Each
Follow this simple hierarchy:
- Default to
constfor everything - Use
letonly when you need to reassign - Never use
varin new code
This approach makes your code more predictable. When you see const, you know the binding won’t change. When you see let, it signals that reassignment happens somewhere.
Here’s a real-world example showing the refactoring process:
// Old code with var
function processUserData(users) {
var results = [];
for (var i = 0; i < users.length; i++) {
var user = users[i];
var processedUser = {
id: user.id,
name: user.name.toUpperCase(),
status: 'active'
};
results.push(processedUser);
}
return results;
}
// Refactored with let and const
function processUserData(users) {
const results = []; // Won't be reassigned
for (let i = 0; i < users.length; i++) { // i will be reassigned
const user = users[i]; // Won't be reassigned
const processedUser = { // Won't be reassigned
id: user.id,
name: user.name.toUpperCase(),
status: 'active'
};
results.push(processedUser); // Mutating is fine
}
return results;
}
// Even better with modern JavaScript
function processUserData(users) {
return users.map(user => ({
id: user.id,
name: user.name.toUpperCase(),
status: 'active'
}));
}
Common Pitfalls and Best Practices
The classic closure problem demonstrates why var is dangerous in loops:
// With var - broken
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (all closures reference the same i)
// With let - works correctly
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2 (each iteration gets its own i)
Forgetting to declare a variable creates an accidental global:
function oops() {
message = 'Hello'; // No var/let/const - creates global!
}
oops();
console.log(window.message); // 'Hello' in browsers
Always use strict mode to catch this error:
'use strict';
function oops() {
message = 'Hello'; // ReferenceError: message is not defined
}
Configure ESLint to enforce best practices:
{
"rules": {
"no-var": "error",
"prefer-const": "error"
}
}
The no-var rule prevents using var entirely, while prefer-const suggests using const when variables aren’t reassigned.
Understanding var, let, and const is fundamental to writing modern JavaScript. The rules are straightforward: use const by default, let when you need reassignment, and treat var as a historical artifact. Your code will be more predictable, bugs will be easier to catch, and other developers will have an easier time understanding your intent.