Articles

var vs let vs const in JavaScript

var, let, and const differ in scope, hoisting behavior, and reassignment rules. Here's what each one actually does and when to reach for it.

Takina Takina · · 4 min read
A code editor showing component source code

var, let, and const all declare variables in JavaScript, but they differ in three ways that matter constantly in practice: what scope the variable belongs to, whether it’s usable before its declaration line runs, and whether it can be reassigned. var is function-scoped and quietly forgiving about redeclaration; let and const are block-scoped and much stricter, with const additionally locking the binding against reassignment. Modern JavaScript defaults to const, falls back to let when reassignment is genuinely needed, and treats var as legacy.

Scope: function vs block

var is scoped to the nearest enclosing function (or the global scope, if declared outside any function) — not to the block it’s written in. This means a var declared inside an if block or a for loop leaks out into the surrounding function:

if (true) {
  var x = 1;
}
console.log(x); // 1 — leaked out of the if block

let and const are scoped to the nearest enclosing block — anything delimited by { }, including if statements, loops, and standalone blocks:

if (true) {
  let y = 1;
}
console.log(y); // ReferenceError — y doesn't exist out here

Block scoping is almost always what you actually want. It keeps a variable’s visibility matched to the code that logically needs it, and it’s why let/const catch a whole category of bugs that var simply allows.

Hoisting and the temporal dead zone

All three are hoisted — technically registered at the top of their scope before the code runs — but they behave differently when accessed before their declaration line.

var declarations are hoisted and initialized to undefined immediately, so reading one before its declaration doesn’t throw, it just gives you undefined:

console.log(a); // undefined
var a = 5;

let and const are hoisted too, but they stay uninitialized until their declaration line actually executes — accessing them before that point throws a ReferenceError rather than silently returning undefined. That window between the start of the scope and the declaration line is called the temporal dead zone:

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;

This is a deliberate strictness improvement — a ReferenceError surfaces a bug immediately, instead of letting a stray undefined propagate silently through the rest of a function.

Reassignment and the classic loop bug

var and let can both be reassigned. const cannot be reassigned after its initial assignment — but that only locks the binding, not the value itself: a const object or array can still have its contents mutated, just not be replaced wholesale.

const arr = [1, 2];
arr.push(3);     // fine — mutating the array
arr = [4, 5];     // TypeError — reassigning the binding

The interaction between scope and closures produces the single most common var gotcha, usually seen in a loop with an asynchronous callback:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// logs: 3, 3, 3 — every callback shares the same function-scoped `i`
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// logs: 0, 1, 2 — each iteration gets its own block-scoped `i`

With var, every callback closes over the same variable, and by the time any of them run, the loop has already finished and i sits at its final value. With let, each loop iteration creates a fresh binding, so each closure captures the value it actually saw during that iteration. This one difference is responsible for a large share of “why does my loop print the same number every time” bug reports predating let’s introduction.

Redeclaration

var allows redeclaring the same variable name in the same scope without complaint — the second declaration just overwrites the first. let and const throw a SyntaxError on redeclaration in the same scope, which catches accidental name collisions that var would otherwise mask.

Comparison at a glance

varletconst
ScopeFunctionBlockBlock
Hoisting behaviorInitialized to undefinedTemporal dead zoneTemporal dead zone
ReassignableYesYesNo (binding only)
Redeclarable in same scopeYesNoNo
Attaches to global object (window/globalThis) when globalYesNoNo
Recommended defaultAvoid in new codeWhen reassignment is neededDefault choice

Which one to actually use

The practical guidance most style guides and linters (including ESLint’s prefer-const and no-var rules) converge on: reach for const by default, since most bindings are never reassigned and const documents that intent while catching accidental reassignment as an error. Use let when a variable genuinely needs to change — loop counters, accumulators, values reassigned across branches. Treat var as legacy syntax that exists for backward compatibility with pre-ES2015 code; there’s essentially no situation in new code where var’s function-scoping and loose hoisting behavior is preferable to the block-scoped alternatives.

The takeaway

var is function-scoped, initializes to undefined when hoisted, and can be redeclared and reassigned freely — behavior that made sense when it was JavaScript’s only option, but causes real bugs like loop variables leaking their final value into async callbacks. let and const fix that with block scoping and a temporal dead zone that turns “used before declared” into an immediate error instead of a silent undefined. Default to const, use let when reassignment is actually needed, and there’s little reason to reach for var in code written today.

Takina Takina · · 5 min read

Promise.all() vs allSettled() vs race() Compared

Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

JavaScript Spread vs Rest Operators, Explained

The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

ResizeObserver API Explained: Watching Element Size

The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.

#Web Development #JavaScript #Frontend