JavaScript Variables: let, const, var — Complete Guide
Variables are how JavaScript stores and manages data. This is one of the most important lessons in the entire roadmap because understanding variables means understanding scope, hoisting, and mutation — three concepts that cause the majority of bugs in JavaScript code.
What is a variable?
A variable is a named container for a value. You give it a name, store something in it, and use that name later to access the value:
let age = 30; // store the number 30 in a container named "age"
console.log(age); // 30
age = 31; // change the value
console.log(age); // 31
Think of a variable like a labeled box. The label is the variable name. The box holds the value. You can look at what is in the box (read), put something new in the box (assign), or give the box to someone else (pass to a function).
Three ways to declare variables
JavaScript has three keywords for declaring variables: let, const, and var. Here is the rule you should follow:
- Use
constby default — for values that should not be reassigned - Use
letwhen you need to reassign — for values that will change - Never use
var— it is the old way and has confusing behavior
const — the default choice
const PI = 3.14159;
const name = 'Ada';
const colors = ['red', 'green', 'blue'];
const user = { name: 'Ada', age: 30 };
// PI = 3.14; // TypeError: Assignment to constant variable
// BUT: const does not mean immutable!
// You can still modify the CONTENTS of objects and arrays:
colors.push('yellow'); // This works! The array itself changes.
user.age = 31; // This works! The property changes.
// colors = ['new', 'array']; // This fails. You cannot reassign the variable.
Key insight: const prevents reassignment, not mutation. You cannot point the variable at a different value, but you can modify the contents of objects and arrays. This is one of the most misunderstood concepts in JavaScript.
let — for values that change
let score = 0;
score = 10; // OK: reassignment is allowed
score += 5; // OK: score is now 15
let counter = 0;
for (let i = 0; i < 5; i++) {
counter += i;
}
// Common use cases for let:
// - Loop counters
// - Accumulators (sum, total)
// - State that updates over time
// - Conditional assignments
let message;
if (score > 10) {
message = 'High score!';
} else {
message = 'Keep trying';
}
var — the legacy keyword (avoid)
var name = 'Ada'; // works, but do not use this
// Why var is problematic:
// 1. It is function-scoped, not block-scoped
// 2. It hoists the declaration (but not the value)
// 3. It allows re-declaration
// 4. It attaches to the window object in browsers
// All of these surprises are avoided with let/const
Scope: where variables live
Scope determines where a variable is accessible. This is the most important concept to understand about variables.
Block scope (let and const)
A block is anything between curly braces {}. Variables declared with let or const are only accessible inside the block where they are declared:
if (true) {
let blockVar = 'I exist only here';
const alsoBlockVar = 'Me too';
console.log(blockVar); // 'I exist only here'
}
// console.log(blockVar); // ReferenceError: blockVar is not defined
for (let i = 0; i < 3; i++) {
// i only exists inside this loop
}
// console.log(i); // ReferenceError: i is not defined
Function scope (var)
var ignores blocks and is scoped to the nearest function:
if (true) {
var leakyVar = 'I escaped the block!';
}
console.log(leakyVar); // 'I escaped the block!' — var leaked out!
for (var j = 0; j < 3; j++) {}
console.log(j); // 3 — j leaked out of the for loop!
// This is why var causes bugs. let/const would not allow this.
Global scope
Variables declared outside any function or block are global — accessible everywhere:
const globalConfig = 'I am everywhere';
function example() {
console.log(globalConfig); // accessible here
}
// Globals are generally bad because:
// - Any code can modify them
// - Name collisions between scripts
// - Hard to track where values change
// Minimize globals. Use modules instead (covered later).
Scope chain
When JavaScript looks for a variable, it searches outward through enclosing scopes:
const a = 'global';
function outer() {
const b = 'outer';
function inner() {
const c = 'inner';
console.log(c); // 'inner' — found in current scope
console.log(b); // 'outer' — found in parent scope
console.log(a); // 'global' — found in grandparent scope
}
inner();
}
outer();
Hoisting: the invisible reordering
Hoisting is JavaScript's behavior of moving declarations to the top of their scope before code executes. But it works differently for var, let/const, and functions.
var hoisting
console.log(x); // undefined (not an error!)
var x = 5;
console.log(x); // 5
// JavaScript sees this as:
// var x; // declaration hoisted to top
// console.log(x); // undefined (declared but not assigned yet)
// x = 5; // assignment stays in place
// console.log(x); // 5
let/const hoisting (Temporal Dead Zone)
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
console.log(y); // 5
// let and const ARE hoisted, but they sit in a "Temporal Dead Zone" (TDZ)
// from the start of the block until the declaration is reached.
// Accessing them in the TDZ throws a ReferenceError.
// This is GOOD — it catches bugs early.
Function hoisting
// Function DECLARATIONS are fully hoisted — you can call them before they are defined:
greet(); // 'Hello!'
function greet() {
console.log('Hello!');
}
// Function EXPRESSIONS are NOT hoisted (they follow const/let rules):
// sayBye(); // ReferenceError
const sayBye = function () {
console.log('Bye!');
};
Naming rules and conventions
What is allowed
// Valid names
let firstName = 'Ada';
let _private = true;
let $element = document.body;
let camelCaseIsStandard = true;
let MAX_SIZE = 100; // UPPER_SNAKE for true constants
// Invalid names
// let 1stName = 'Ada'; // cannot start with a number
// let my-var = 'x'; // hyphens not allowed
// let class = 'x'; // reserved keywords not allowed
JavaScript naming conventions
- camelCase — for variables and functions:
userName,getUser(),isActive - PascalCase — for classes and components:
UserProfile,EventEmitter - UPPER_SNAKE_CASE — for true constants (values known at compile time):
MAX_RETRIES,API_URL - _prefix — informally indicates "private" (not enforced):
_internalState
Destructuring assignment
A powerful way to extract values from objects and arrays into variables:
// Object destructuring
const user = { name: 'Ada', age: 30, city: 'London' };
const { name, age, city } = user;
console.log(name); // 'Ada'
console.log(age); // 30
// With renaming
const { name: userName, age: userAge } = user;
console.log(userName); // 'Ada'
// With defaults
const { name: n, role = 'user' } = user;
console.log(role); // 'user' (default, since user.role is undefined)
// Array destructuring
const rgb = [255, 128, 0];
const [red, green, blue] = rgb;
console.log(red); // 255
console.log(green); // 128
// Skip elements
const [first, , third] = [1, 2, 3];
console.log(third); // 3
// Swap variables (no temp needed!)
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2, 1
Common mistakes and how to avoid them
Mistake 1: Confusing const with immutability
const arr = [1, 2, 3];
arr.push(4); // This works! const prevents reassignment, not mutation.
// arr = [5, 6]; // This fails.
// If you need true immutability:
const frozen = Object.freeze([1, 2, 3]);
// frozen.push(4); // TypeError (in strict mode) or silently fails
Mistake 2: var in loops (the classic closure bug)
// BUG: all timeouts print 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (not 0, 1, 2!)
// Because var is function-scoped — there is only ONE i
// FIX: use let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2 (each iteration gets its own i)
Mistake 3: Using variables before declaration
// With let/const, JavaScript catches this immediately:
// console.log(x); // ReferenceError
// let x = 5;
// With var, it silently gives you undefined — a much harder bug to find:
console.log(y); // undefined (no error, just wrong)
var y = 5;
Mistake 4: Accidental globals
function buggy() {
mistake = 'oops'; // No let/const/var — creates a GLOBAL variable!
}
buggy();
console.log(mistake); // 'oops' — leaked globally
// FIX: Always use strict mode
'use strict';
function fixed() {
// mistake = 'oops'; // ReferenceError in strict mode
const correct = 'much better';
}
What to learn next
Now you understand how variables work, how scope controls access, and how hoisting can surprise you. Next:
- Data types — learn what kinds of values JavaScript variables can hold.
- Type coercion — understand how JavaScript converts between types automatically.