JavaScript Variables

A variable is a named container that stores a value in memory so your program can read it, change it, and reuse it without retyping the value everywhere. Every piece of data your code works with — a user’s name, a running total, a list of products — needs to live in a variable so it can be referred to by a meaningful name. JavaScript gives you three keywords for declaring variables: var, let, and const. Knowing exactly how each one behaves, especially around scope and timing, is one of the most important foundations for writing reliable JavaScript.

Overview / How Variables Work

Declaring a variable happens in two logical steps, even when you write them on one line: declaration, where the JavaScript engine reserves a name in a particular scope, and initialization, where you give it a starting value. let age; declares age without a value (it becomes undefined), while let age = 30; declares and initializes it in one step.

JavaScript has three ways to declare a variable:

  • var — the original keyword from the earliest versions of JavaScript. It is function-scoped (or globally scoped if declared outside any function), and it can be redeclared and reassigned freely.
  • let — introduced in ES6 (2015). It is block-scoped, meaning it only exists inside the nearest pair of curly braces: an if block, a for loop, a function body, or a bare block. It can be reassigned but not redeclared in the same scope.
  • const — also introduced in ES6, also block-scoped. It must be initialized when declared, and its binding can never be reassigned afterward.

Scope determines where in your code a variable is visible. JavaScript has global scope (declared outside any function or block), function scope (visible anywhere inside the function that declares it), and, since ES6, block scope (visible only inside the braces where it was declared). This is the biggest practical difference between var and let/const: a var declared inside an if block leaks out and stays visible for the rest of the enclosing function, while a let or const does not.

Internally, before your code actually runs, the JavaScript engine performs a creation phase where it scans the current scope and registers every declared variable in memory ahead of time. This is called hoisting. var declarations are hoisted and initialized to undefined immediately, which is why you can reference a var before its declaration line without an error — you just get undefined back. let and const are hoisted too, but they are not initialized. They sit in a temporal dead zone (TDZ) from the start of their scope until the line where they are actually declared. Accessing them during the TDZ throws a ReferenceError. This is a deliberate safety feature: it stops you from accidentally using a variable before it has a sensible value.

Finally, it matters what kind of value a variable holds. Primitive values (numbers, strings, booleans, null, undefined, symbol, bigint) are copied by value — assigning one variable to another copies the value itself. Objects and arrays are stored by reference — the variable holds a pointer to data in memory, not the data itself. This is why const lets you mutate the contents of an array or object freely (you are not changing the binding, only what it points to) but never lets you point the variable at a brand-new array or object.

Syntax

let variableName = value;
const CONSTANT_NAME = value;
var legacyName = value;

let a, b, c;         // multiple declarations
let x = 1, y = 2;    // multiple with initializers
Keyword Scope Reassignable Redeclarable Hoisted
var Function / global Yes Yes Yes, initialized to undefined
let Block Yes No Yes, stays in the TDZ until declared
const Block No No Yes, stays in the TDZ until declared

Naming Rules

  • Names may contain letters, digits, $, and _, but cannot start with a digit.
  • Names are case-sensitive — total and Total are different variables.
  • Reserved words such as let, class, and return cannot be used as variable names.
  • By convention, use camelCase for ordinary variables and functions, and UPPER_SNAKE_CASE for constants that represent fixed, unchanging values.

Examples

Example 1: Declaring and reassigning

let score = 10;
score = score + 5;

const username = 'nova92';
console.log(`${username} has a score of ${score}`);
Output:
nova92 has a score of 15

Here score is declared with let because its value needs to change, so reassigning it with score = score + 5 is legal. username is declared with const because it never needs to change, and a template literal is used to combine both values into one readable string.

Example 2: Function scope vs. block scope

function demoScope() {
  if (true) {
    var functionScoped = 'I leak out of the block';
    let blockScoped = 'I stay inside the block';
    console.log(functionScoped);
    console.log(blockScoped);
  }
  console.log(functionScoped);
  try {
    console.log(blockScoped);
  } catch (error) {
    console.log(error.message);
  }
}

demoScope();
Output:
I leak out of the block
I stay inside the block
I leak out of the block
blockScoped is not defined

Both variables are declared inside the if block, but only functionScoped (declared with var) is still readable once execution leaves the block, because var only respects function boundaries. blockScoped (declared with let) ceases to exist outside its block, so accessing it throws a ReferenceError, caught here and logged as a plain message.

Example 3: const with arrays and objects

const cart = ['apple', 'banana'];
cart.push('cherry');
console.log(cart);

const totalItems = cart.length;
console.log(`Cart has ${totalItems} items: ${cart.join(', ')}`);
Output:
[ 'apple', 'banana', 'cherry' ]
Cart has 3 items: apple, banana, cherry

cart is declared with const, yet cart.push('cherry') works fine. That’s because push mutates the array that cart points to; it doesn’t reassign the cart variable itself. const only locks the binding — the fact that the name cart always refers to that one array in memory — not the contents of what it refers to.

Under the Hood: Hoisting and the Temporal Dead Zone

console.log(typeof hoistedVar); // undefined due to hoisting
var hoistedVar = 'I am hoisted';

try {
  console.log(hoistedLet);
} catch (error) {
  console.log(error.message);
}
let hoistedLet = 'I am in the temporal dead zone';
Output:
undefined
Cannot access 'hoistedLet' before initialization

Before executing this file line by line, the engine scans the scope and registers both hoistedVar and hoistedLet in memory. hoistedVar is immediately set to undefined, so reading it early gives 'undefined' from typeof rather than an error. hoistedLet, however, is registered but left uninitialized and marked as being in the temporal dead zone. Trying to read it before its declaration line executes throws a ReferenceError with a message that explicitly calls out the TDZ. This is why relying on hoisting with var is dangerous — the code runs but silently gives you undefined — while let/const fail loudly and immediately, which is easier to debug.

Common Mistakes

Mistake 1: Using var in a loop that creates functions

A classic bug happens when a loop declared with var is used to create functions (callbacks, event handlers, timers) that reference the loop variable. Because var is function-scoped, all the created functions share the exact same variable — not a fresh copy per iteration.

const timers = [];
for (var i = 0; i < 3; i++) {
  timers.push(() => console.log(i));
}
timers.forEach(fn => fn());
Output:
3
3
3

By the time any of the functions actually run, the loop has already finished and i is 3 for all of them. Switching to let fixes it, because let creates a brand-new binding of i for each iteration of the loop:

const timers = [];
for (let i = 0; i < 3; i++) {
  timers.push(() => console.log(i));
}
timers.forEach(fn => fn());
Output:
0
1
2

Mistake 2: Assuming const makes a value immutable

A very common misconception is that const freezes a value. It doesn’t — it only prevents the variable name from being pointed at something new. Writing const settings = { theme: 'dark' }; settings = {}; throws TypeError: Assignment to constant variable., because that reassigns the binding. But mutating a property, like settings.theme = 'light';, works fine, because the binding itself never changes.

const settings = { theme: 'dark', fontSize: 14 };
settings.theme = 'light';
console.log(settings);

const frozenSettings = Object.freeze({ theme: 'dark' });
frozenSettings.theme = 'light';
console.log(frozenSettings.theme);
Output:
{ theme: 'light', fontSize: 14 }
dark

If you actually need to prevent an object’s contents from being changed, const alone is not enough — use Object.freeze(). Note that in a normal (non-strict-mode) script, attempting to write to a frozen object’s property fails silently instead of throwing, which is why frozenSettings.theme is still 'dark' afterward.

Best Practices

  • Default to const for every variable; only switch to let when you know the value must change later.
  • Avoid var in modern code — its function scoping and hoisting-to-undefined behavior cause bugs that block scoping prevents.
  • Declare variables as close as possible to where they’re first used, rather than all at the top of a function.
  • Give variables descriptive, intention-revealing names (userCount, not uc or x).
  • Never rely on hoisting — always declare a variable before you use it, even though var technically allows otherwise.
  • Use UPPER_SNAKE_CASE for values that are conceptually constants across your whole program, like configuration flags or fixed limits.
  • Remember that const protects the binding, not the value — use Object.freeze() if you need true immutability for an object.

Practice Exercises

  • Write a function that declares a let counter variable, increments it three times inside a loop, and logs its final value. Then try rewriting the loop variable as const and explain in a comment why it fails.
  • Predict, then verify, what the following logs: declare var x inside an if block, try to read it right after the block outside the function, and also try reading a let variable declared inside that same block from outside it.
  • Create a const object representing a product (name, price, inStock). Write code that updates its price property directly, then write code that attempts to reassign the whole variable to a new object and observe the error.

Summary

  • Variables store values under a name using var, let, or const.
  • var is function-scoped and hoisted to undefined; let and const are block-scoped and stay in the temporal dead zone until declared.
  • let can be reassigned; const cannot be reassigned but its object/array contents can still be mutated.
  • Accessing a let or const before its declaration throws a ReferenceError; accessing a var early just gives undefined.
  • Prefer const by default, use let when reassignment is needed, and avoid var in modern code.