Variables

A named container for storing a value your program can reuse.

What is it?

Programs need to remember things — a username, a score, an item price. A variable is simply a name you give to a piece of information so you can use it again later, without retyping the value every time.

In JavaScript, you create a variable with let or const:

  • Use let when the value might change later.
  • Use const when the value should stay the same after it's set.

There's also an older keyword, var, but modern JavaScript mostly avoids it in favor of let and const, which behave more predictably.

Explain like I'm 10

A variable is like a labeled jar. You write a label on it (the variable name) and put something inside (the value). Later, you just read the label to find what's inside.

Examples

Declaring variables

let score = 0;
score = score + 10; // score is now 10

const username = "amara";
// username = "someone-else"; // ❌ this would cause an error

score can change because it's declared with let. username cannot be reassigned because it's declared with const.

Using a variable to avoid repeating a value

const taxRate = 0.08;

const price1 = 20;
const total1 = price1 + price1 * taxRate;

const price2 = 45;
const total2 = price2 + price2 * taxRate;

taxRate is defined once and reused in both calculations — if the tax rate ever changes, there's exactly one place to update it.

How it works

When JavaScript sees let score = 0, it sets aside a small space in memory, labels that space "score", and stores the value 0 there. Whenever your code uses the word score afterward, JavaScript looks up that labeled space and uses whatever value currently lives there.

Why does it exist?

Without variables, you'd have to write the same literal values everywhere, and you'd have no way to store something that changes while the program runs — like a running total or the current user's name.

When to use it

Use a variable any time you need to store a value so you can use it again later — a running total, a piece of user input, a flag that tracks whether something happened. Reach for const by default, and switch to let only once you know the value genuinely needs to change.

When not to use it

You don't need a variable for a value you use exactly once and never refer to again — sometimes it's clearer to just write the value directly where it's needed. And avoid var in new code: there's no situation in modern JavaScript where var behaves better than let/const.

Common mistakes

  • Trying to reassign a const variable — it will throw an error.

  • Using a variable before declaring it.

  • Picking unclear names like x or data1 instead of descriptive ones like cartTotal.

Practice exercises

  1. Easy:

    Declare a const variable called age and log it to the console.

  2. Medium:

    Declare a let variable called count, then write code that increases it by 1 three times.

  3. Hard:

    Write a small script that stores a product's price and quantity in variables and logs the total cost.

Interview questions

What's the difference between `var`, `let`, and `const`?

var is function-scoped (or global) and can be redeclared and reassigned; let is block-scoped and can be reassigned but not redeclared in the same scope; const is block-scoped like let but cannot be reassigned after its initial value is set. All three differ mainly in scope and whether reassignment/redeclaration is allowed — not in what kinds of values they can hold.

Can you declare the same variable name twice in the same scope with `let`?

No — redeclaring a let (or const) binding in the same scope throws a SyntaxError ("Identifier has already been declared"). var, by contrast, allows redeclaring the same name in the same scope with no error at all.

What happens if you try to reassign a `const` variable?

JavaScript throws a TypeError: Assignment to constant variable. at the moment the reassignment runs — const locks the binding itself, not just discourages changing it.

What's the difference between declaring a variable and initializing it?

Declaration is telling the engine a name exists in a scope (e.g. let x); initialization is giving it its first value (e.g. x = 5, or combined as let x = 5). The two can happen on separate lines for var and let — a var starts as undefined between declaration and initialization, while a let/const stays inaccessible (in the TDZ) until the initialization line actually runs.

What is variable scope, in plain terms?

Scope is the region of code where a given variable name is visible and usable. Code outside that region either can't see the variable at all, or sees a different variable that happens to share the name.

What is function scope, and which keyword creates it?

Function scope means a variable is visible anywhere inside the function it was declared in, regardless of how many nested blocks (if, for, etc.) it passes through — var is function-scoped, so a var declared inside an if block is still visible for the rest of the whole function.

What is block scope, and which keywords create it?

Block scope means a variable is only visible inside the nearest pair of curly braces { } it was declared in — an if block, a for loop body, or any standalone { }. let and const are block-scoped.

What is global scope?

The outermost scope, not nested inside any function or block — a variable declared there is visible from anywhere else in the file (and, for var or an implicit global, becomes a property of the global object in non-module scripts).

What happens if you try to use a variable that was never declared anywhere?

JavaScript throws a ReferenceError: x is not defined the moment that line runs — there's no fallback value, unlike a declared-but-unassigned variable which is simply undefined.

What does a `ReferenceError` actually indicate?

That the code referenced a name that doesn't exist as a binding in any scope currently visible to it — either it was never declared, or it's a let/const being accessed before its declaration has run (the TDZ case), which throws this same error type.

Are `let` and `const` hoisted the same way `var` is?

They're hoisted in the sense that the engine registers the binding at the top of the scope during compilation, but unlike var they are not initialized to undefined at that point — they stay uninitialized in the Temporal Dead Zone until the actual declaration line executes, so accessing them earlier throws instead of returning undefined.

What is the Temporal Dead Zone (TDZ)?

The span of code between the start of a scope and the line where a let/const variable is actually declared, during which the variable exists (it's hoisted) but cannot be read or written — any access in that window throws a ReferenceError.

If `var`, `let`, and `const` are all hoisted, why does only `var` let you access the variable before its declaration line?

Hoisting for var initializes the binding to undefined immediately, so an early read just gets undefined. Hoisting for let/const only reserves the name — it deliberately leaves it uninitialized until the declaration line runs, so an early read hits the TDZ and throws instead.

What's the difference between reassignment and mutation?

Reassignment replaces what a variable points to entirely (x = newValue); mutation changes the contents of the value a variable already points to, without changing which value that is (e.g. arr.push(1) or obj.key = 2). const blocks reassignment but has no say over mutation.

Can you change a property on an object declared with `const`? Can you reassign the variable to a different object?

You can freely change, add, or delete its properties (obj.name = "new") because that's mutation, not reassignment. You cannot do obj = { other: "object" } — that's a reassignment of the binding itself, which const forbids.

Can you `.push()` to a `const` array? Can you reassign it to a new array?

Yes to pushing (and .pop(), .splice(), index assignment, etc.) — those mutate the existing array in place. No to reassigning it to a different array or a new literal (arr = [1, 2]) — const only ever locks the binding, not the array's contents.

What is variable shadowing?

When a variable declared in an inner scope has the same name as one in an outer scope, the inner one shadows the outer one for the rest of that inner scope — code inside sees only the inner variable, and the outer one is unaffected and reappears once the inner scope ends.

What happens when you declare a `var` inside an `if` block?

It's not scoped to that block at all — because var is function-scoped, the declaration is hoisted to the top of the nearest enclosing function (or the global scope), so the variable is accessible even outside and after the if block, which frequently surprises people expecting block scoping.

What is lexical scope?

Scope determined by where code is physically written in the source, not by how or from where it's called — a function can access variables from the scopes it was literally nested inside when it was defined, regardless of where it's later invoked from.

What does it mean for scopes to be nested?

An inner scope (a function or block) sits inside an outer one and can read variables from every scope surrounding it, out to the global scope — but the reverse isn't true: an outer scope can't see variables declared inside an inner one.

If you declare a variable with `let` inside a function, can code outside that function access it?

No — once the function (or block) ends, that variable is out of scope and inaccessible from the outside; there is no way to reach it except by the function explicitly returning or exposing its value.

What happens if you assign to a name with no `let`, `const`, or `var` at all, like `total = 5;`?

In non-strict mode, JavaScript silently creates an undeclared global variable — no error, but a variable that pollutes the global scope and wasn't intentionally declared anywhere. In strict mode ("use strict", or inside any ES module or class), the same line throws a ReferenceError instead.

Why is relying on implicit globals (assigning without a declaration keyword) considered risky?

It creates a variable outside of any controlled scope, invisible at a glance to anyone reading the surrounding function, and it can silently collide with a same-named variable elsewhere in a large codebase — bugs from this are exactly what strict mode's ReferenceError behavior for undeclared assignment is designed to catch early.

Explain the TDZ in terms of lexical environments.

When the engine enters a scope, it creates a lexical environment and immediately registers every let/const name declared anywhere in that scope, but marks each one's binding as uninitialized rather than giving it a value. Only when execution actually reaches the declaration statement does the binding get marked initialized and receive its value — any lookup of that name before that point finds an uninitialized binding and throws, which is the TDZ in mechanical terms.

Why does JavaScript bother having a TDZ instead of just initializing `let`/`const` to `undefined` like `var`?

It turns a class of bugs — reading a variable before the line that's supposed to set it up — into an immediate, loud error instead of a silent undefined that might not surface a problem until much later. It also keeps const semantically consistent: a const implicitly initialized to undefined and then "assigned" its real value later would effectively be reassigned, which const isn't supposed to allow.

What's the key hoisting difference between a function declaration and a `let`/`var` declaration?

A function declaration is hoisted along with its entire body, so it can be called before the line it's written on. A var declaration is hoisted but only initialized to undefined, and a let/const declaration is hoisted but left uninitialized (TDZ) — only the function declaration gives you working behavior early; the others just give you the name reserved.

Why does using `let` instead of `var` fix the classic loop-and-`setTimeout` bug?

With var, there's a single shared binding for the whole loop, so by the time any callback runs, every one of them sees the loop variable's final value. With let, the loop creates a brand-new binding scoped to each individual iteration, so each callback closes over its own separate copy holding that iteration's value.

Can two variables with the same name coexist in nested scopes at the same moment?

Yes — each scope has its own independent binding, so an inner let x and an outer let x are genuinely two different storage locations that happen to share a name. Which one a piece of code sees is resolved lexically: the engine looks for the name starting in the current scope and works outward, stopping at the first match — the innermost one wins for any code inside it.

Output-prediction: what does `console.log(x); var x = 5;` print, and why not a `ReferenceError`?

It logs undefined, not a ReferenceError. var x is hoisted to the top of the scope and initialized to undefined immediately, so the console.log sees that placeholder value — the assignment x = 5 hasn't run yet at that point.

Output-prediction: what does `console.log(y); let y = 5;` do?

It throws ReferenceError: Cannot access 'y' before initialization. y is hoisted but stays in the Temporal Dead Zone until its declaration line runs, so reading it one line earlier hits the TDZ instead of returning undefined.

Output-prediction: what does `let count = 1; { let count = 2; console.log(count); } console.log(count);` log?

It logs 2, then 1. The { } block creates a new scope, and let count = 2 inside it shadows the outer count for the duration of that block; once the block ends, that inner binding is gone and the final console.log sees the untouched outer count, still 1.

Output-prediction: what does `for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); }` log?

It logs 3, 3, 3. All three callbacks close over the same single var i binding (function-scoped, shared across every iteration), and by the time any of them actually runs — after the loop has finished — i has already reached 3.

Output-prediction: if you replace `var i` with `let i` in the same loop, `for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); }`, what does it log now, and why?

It logs 0, 1, 2. let in a for loop's header creates a fresh binding for each iteration, so each arrow function captures a separate i holding that iteration's own value at the time it was created.

Output-prediction: why does `const point = { x: 1 }; point = { x: 2 };` throw, and what's the exact error?

It throws TypeError: Assignment to constant variable. — point = is a reassignment of the binding itself, which const forbids, regardless of the fact that the new value is a similarly-shaped object. Writing point.x = 2 instead would work fine, since that mutates the existing object rather than reassigning the binding.

Trap: a teammate declares a loop counter with `const i` in a classic `for` loop and it immediately throws. Why?

A standard for (const i = 0; i < 3; i++) throws a TypeError on the very first iteration, because i++ tries to reassign i, which const doesn't allow. const only works as a for loop variable in a for...of/for...in loop, where a fresh, separately-initialized binding is created for each iteration rather than being reassigned in place.