Variables: let, const, and var
Declare and assign values with let, const, and var — and understand block scope, reassignment, and why var is legacy.
Variables are how you name values the program must remember. Sounds trivial — but most subtle JavaScript bugs trace back to scope (who can see the name?) and mutability (can the binding or the value change?). This lesson builds the mental model production teams expect: const by default, let when you must reassign, and var only when reading legacy code.
Variables name values in memory
A variable is a binding between an identifier and a value in memory — not the value itself. When you write const user = { name: "A" }, the name user points at an object on the heap. Reassigning user would point the name elsewhere; mutating user.name changes the object both names would share if you copied the reference. JavaScript gives you three declaration keywords with different scope and hoisting rules; only const and let belong in code you write today.
| Keyword | Reassign? | Scope | Use in 2026 code? |
|---|---|---|---|
| const | Binding fixed; object contents can mutate | Block | Default choice |
| let | Yes | Block | When reassignment is required |
| var | Yes | Function (or global) | Legacy only — avoid |
const — default to immutability of the binding
const means the identifier cannot be rebound to another value. It does not mean the value is deeply immutable — const arr = [] still allows push and pop because you are mutating the array object, not replacing the arr binding. Teams use const to signal intent: this name always refers to the same thing for the rest of the block. When you need to reassign (counters, swapping values, re-fetching in a loop), switch to let in that narrow case.
{ name: 'Sangam', score: 11 }let — block-scoped reassignment
Block scope means a variable declared inside { } dies at the closing brace. That matches how humans read indentation — a message declared inside an if block should not leak into the rest of the function. let was added to JavaScript specifically to fix the visual vs actual scope mismatch that var created for decades.
inside block
1var — function scope and the classic loop bug
var ignores block boundaries. Its declaration is hoisted to the top of the enclosing function (or global scope) and initialized to undefined until the assignment line runs. That is why var i in a loop plus async callbacks produced the classic bug: one shared i, not one per iteration. let creates a fresh binding per iteration in for loops, which is why modern code behaves intuitively with closures inside loops.
var: 3
var: 3
var: 3
let: 0
let: 1
let: 2Hoisting preview (full lesson later)
Hoisting is often explained poorly as "functions move to the top." More precisely: the engine registers bindings during compile phase before execution. var bindings exist as undefined early; let/const bindings exist but sit in the temporal dead zone until their declaration line runs — accessing them before that line throws ReferenceError, which prevents the silent undefined bugs var allowed.
undefined
block scopedNaming and declaration style
- Use camelCase for variables: userCount, isActive
- Use UPPER_SNAKE for true constants: MAX_RETRIES = 3
- Declare one logical concept per line — avoid comma-chained var a = 1, b = 2
- Initialize when you know the value — reduces undefined bugs
- 1Default to const; reach for let only when reassigning
- 2Avoid var in new code — block scope matches visual structure
- 3const does not deep-freeze objects — clone if you need immutability
- 4Next lesson: primitive types, objects, and typeof