Back to JavaScript
JavaScript
easy
very high
junior

What are the differences between var, let, and const in JavaScript?

`var` is function-scoped and hoisted to `undefined`. `let`/`const` are block-scoped with a temporal dead zone. `const` forbids reassignment but the value can still be mutated.

5 min read·~8 min to think through

Three axes to compare:

varletconst
Scopefunctionblockblock
Hoistinghoisted, initialized to undefinedhoisted, but in TDZ until declarationsame as let
Reassignyesyesno
Redeclare in same scopeyes (silently)no (SyntaxError)no

The Temporal Dead Zone (TDZ) is the window between entering a block and the actual let/const line — accessing the binding throws ReferenceError. This catches typos that var would silently let through.

const blocks reassignment of the binding, not mutation of the value. const arr = []; arr.push(1) is fine; const arr = []; arr = [1] is not.

Default to const. Use let only when you actually reassign. Reach for var essentially never in modern code — the only legitimate case is hot-patching legacy scripts that rely on its hoisting.

Code

ts
if (true) {
  var x = 1;   // leaks to enclosing function
  let y = 2;   // dies at the closing brace
}
console.log(x); // 1
console.log(y); // ReferenceError

console.log(z); // ReferenceError — TDZ
let z = 3;
Scope and TDZ
ts
const user = { name: "Ada" };
user.name = "Bea";   // ok — mutating the value
// user = {};        // TypeError — can't reassign the binding
const blocks rebinding, not mutation

Follow-up questions

  • What is the temporal dead zone, and why does it exist?
  • How does hoisting differ between function declarations and var?
  • Why does using let in a for loop fix the classic closure-in-loop bug?

Common mistakes

  • Thinking `const` makes the value immutable — it doesn't.
  • Using `var` in a `for` loop and being surprised that callbacks all see the final value.
  • Believing TDZ means the variable isn't hoisted — it is, but accessing it throws.

Performance considerations

  • TDZ checks have negligible runtime cost; modern engines elide them after the first read.
  • Block scoping enables tighter escape analysis and smaller closure environments — generally a win.

Edge cases

  • `typeof` on a TDZ binding still throws (unlike on a truly undeclared identifier, where it returns 'undefined').
  • `var` declarations at the top of a module bind to the module scope, not the global object — unlike scripts.

Real-world examples

  • ESLint's `prefer-const` codifies the 'default to const' rule across most modern codebases.
  • TypeScript narrowing works better with `const` because the binding can't change between checks.

Senior engineer discussion

At senior level you should be able to talk about how V8 represents these in its environment records, why `let` in a for-loop creates a per-iteration binding (a spec choice for closure correctness), and how this interacts with `for..in` / `for..of` iteration order.

Related questions