Comparison

let vs const vs var in JavaScript: Complete Comparison

Compare let, const, and var in JavaScript: scoping, hoisting, TDZ, reassignment rules, and which one to use in modern code with examples.

let vs const vs var in JavaScript: Complete Comparison

JavaScript offers three ways to declare variables: var, let, and const. Although they look interchangeable, they differ in scope, hoisting behavior, and whether the binding can be reassigned. Choosing the right one prevents bugs and makes intent obvious to anyone reading your code.

Quick comparison table

Featurevarletconst
ScopeFunctionBlockBlock
HoistedYes, initialized to undefinedYes, but in TDZYes, but in TDZ
Redeclarable in same scopeYesNoNo
ReassignableYesYesNo (binding is fixed)
Must initialize at declarationNoNoYes
Creates property on global objectYesNoNo

var: function-scoped and hoisted

var is scoped to the nearest enclosing function (or the global scope). It ignores block boundaries like if or for. Declarations are hoisted to the top of the scope and automatically initialized to undefined, so you can reference them before the line they appear on without a ReferenceError.

console.log(x); // undefined (not an error)
var x = 5;

if (true) {
  var y = 10;
}
console.log(y); // 10 — leaks out of the block

var y = 20; // redeclaration is allowed

Because var leaks out of blocks and silently redeclares, it is easy to introduce subtle bugs — the classic example being a loop counter that all callbacks share.

let: block-scoped and reassignable

let is scoped to the nearest enclosing block (anything between { }). It is hoisted but not initialized, so accessing it before its declaration throws a ReferenceError — this window is called the temporal dead zone (TDZ). You can reassign a let variable, but you cannot redeclare it in the same scope.

console.log(a); // ReferenceError — a is in the TDZ
let a = 1;
a = 2; // OK, reassignment is fine
let a = 3  // ← this line is a parse-time SyntaxError; NONE of the code above runs when it is present; // SyntaxError — cannot redeclare

if (true) {
  let b = 10;
}
console.log(b); // ReferenceError — b does not exist here

const: block-scoped, immutable binding

const behaves like let for scoping and TDZ, but the binding cannot be reassigned after initialization. You must provide a value at the point of declaration. Crucially, const freezes the binding, not the value. If the value is an object or array, its contents are still mutable.

const PI = 3.14159;
PI = 3; // TypeError — assignment to constant

const user = { name: 'Ada' };
user.name = 'Grace';    // OK — mutating the object
user.age = 30;          // OK — adding a property
user = { name: 'Lin' }; // TypeError — rebinding is forbidden

const nums = [1, 2, 3];
nums.push(4); // OK — [1, 2, 3, 4]

If you need a truly immutable object, wrap it with Object.freeze()const alone is not enough.

The temporal dead zone explained

The TDZ is the region between the start of a block and the line where a let or const is declared. During that region the variable exists in scope but cannot be read or written. This design surfaces bugs early: instead of silently returning undefined like var, the engine throws a clear ReferenceError.

{
  // TDZ for value starts here
  // typeof value; // ReferenceError
  let value = 42; // TDZ ends
  console.log(value); // 42
}

Hoisting side by side

function demo() {
  console.log(v); // undefined  — var is hoisted and initialized
  console.log(l); // ReferenceError — let in TDZ
  var v = 1;
  let l = 2;
}

The loop closure trap

This is the canonical reason to prefer let over var:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3 — all callbacks share the same i

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Logs: 0, 1, 2 — each iteration gets a fresh binding

Which one should you use?

  • Use const by default. It communicates that the binding will not change, which is true for most variables in modern code.
  • Use let when you genuinely need to reassign — loop counters that cannot use for..of, accumulators, or values that swap between two states.
  • Avoid var in new code. It exists for backward compatibility. The only common reason to reach for it is maintaining a legacy codebase that already uses it consistently.

Following the const-by-default rule pushes you toward smaller, single-purpose variables and makes accidental reassignment impossible — which is exactly the kind of guardrail JavaScript needed.

let vs const vs var javascript variable declarations temporal dead zone block scope function scope hoisting es6 let const javascript best practices

Explore 300+ Free Tools

Utilko has tools for developers, writers, designers, students, and everyday users — all free, all browser-based.