Skip to main content
Being Idea Innovations
var vs let vs const in JavaScript: Every Difference That Matters
Back to Blog

var vs let vs const in JavaScript: Every Difference That Matters

24 April 20236 min readJavascript
Share:

JavaScript has three ways to declare a variable, and the differences between them cause a surprising share of real bugs. This is the complete comparison — scope, hoisting, reassignment, redeclaration, and the traps that catch people who think they already know the answer.

The short version

  • const — block scoped, cannot be reassigned. Use this by default.
  • let — block scoped, can be reassigned. Use when the value genuinely changes.
  • var — function scoped, legacy. Avoid in new code.

Everything below is why.

Difference 1: scope

var is scoped to the enclosing function. let and const are scoped to the enclosing block — any { }, including those of if, for and while.

function scopeDemo() {
  if (true) {
    var  a = 'var';
    let  b = 'let';
    const c = 'const';
  }

  console.log(a);  // "var"        — leaked out of the if block
  console.log(b);  // ReferenceError
  console.log(c);  // ReferenceError
}

That leak is the single biggest practical argument against var. Variables escape the block you wrote them in and remain visible for the rest of the function, which makes accidental collisions easy.

Difference 2: hoisting and the temporal dead zone

You will often read that "let and const are not hoisted". That is a convenient shorthand, but it is not true, and believing it will confuse you the first time you hit a real TDZ error.

All three are hoisted — the declaration is registered in its scope before any code runs. What differs is what happens if you touch the variable before its line.

console.log(a);   // undefined
console.log(b);   // ReferenceError: Cannot access 'b' before initialization

var a = 1;
let b = 2;

var is initialised to undefined immediately, so an early read silently returns undefined. let and const stay uninitialised until execution reaches the declaration; that window is the temporal dead zone (TDZ), and reading inside it throws.

The difference in error message proves the hoisting. An undeclared variable gives you ReferenceError: b is not defined. A TDZ variable gives you Cannot access 'b' before initialization — the engine knows b exists in this scope, which it could only know if the declaration had been hoisted.

The throw is the better behaviour. A ReferenceError pointing at the exact line beats an undefined that quietly flows through your code and blows up somewhere unrelated.

Difference 3: reassignment

var  a = 1; a = 2;   // fine
let  b = 1; b = 2;   // fine
const c = 1; c = 2;  // TypeError: Assignment to constant variable

const also requires an initialiser — const x; is a syntax error, whereas let x; is fine.

const does not mean immutable

This is the most common misunderstanding. const prevents rebinding the variable. It says nothing about the value's contents:

const user = { name: 'Ada' };
user.name = 'Grace';        // allowed — mutating the object
user.role = 'engineer';     // allowed — adding a property
user = { name: 'Alan' };    // TypeError — rebinding the variable

const list = [1, 2, 3];
list.push(4);               // allowed — list is now [1, 2, 3, 4]
list = [];                  // TypeError

If you want the value itself frozen, that is a separate operation:

const config = Object.freeze({ retries: 3, nested: { timeout: 500 } });
config.retries = 5;          // silently ignored (throws in strict mode)
config.nested.timeout = 900; // still works — freeze is shallow

Note the last line. Object.freeze() is shallow; nested objects need freezing recursively if you want deep immutability.

Difference 4: redeclaration

var a = 1;
var a = 2;      // fine — silently overwrites

let b = 1;
let b = 2;      // SyntaxError: Identifier 'b' has already been declared

Silent redeclaration is genuinely dangerous in a large file: you can clobber a variable hundreds of lines away and never know. let and const turn that into an error before the code ever runs.

Difference 5: the global object

A top-level var in a classic script becomes a property of the global object. let and const do not.

var   x = 1;
let   y = 2;
const z = 3;

console.log(window.x);  // 1
console.log(window.y);  // undefined
console.log(window.z);  // undefined

This matters because var at the top level can overwrite built-in globals. Declaring var name = 'Ada' in a script clobbers window.name, which is a real browser property, and the resulting bugs are baffling.

Inside an ES module none of this applies — module top-level declarations are module-scoped regardless of keyword.

Difference 6: loops and closures

The classic interview question, and a real bug people still ship:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3, 3, 3

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0, 1, 2

With var there is one i shared by every callback. The loop finishes before any timeout fires, so all three read the final value, 3.

With let, the specification creates a fresh binding per iteration and copies the current value into it, so each closure captures its own i. This is special-cased behaviour for let in for loops — not something you could derive from block scoping alone.

Before let existed, the workaround was an IIFE to create a new scope per iteration:

for (var i = 0; i < 3; i++) {
  (function (captured) {
    setTimeout(() => console.log(captured), 0);
  })(i);
}
// 0, 1, 2

const in a for loop

for (const i = 0; i < 3; i++) { }   // TypeError on the first i++

for (const item of items) { }        // fine — new binding each iteration
for (const key in object) { }        // also fine

A C-style for loop reassigns its counter, so const fails. for…of and for…in create a new binding per iteration and never reassign, so const is correct there and is what you should use.

Side by side

Behaviourvarletconst
ScopeFunctionBlockBlock
HoistedYes, as undefinedYes, into TDZYes, into TDZ
Read before declarationundefinedReferenceErrorReferenceError
ReassignableYesYesNo
Redeclarable in same scopeYesNoNo
Initialiser requiredNoNoYes
Creates a global object propertyYesNoNo
New binding per loop iterationNoYesYes (for…of / for…in)

Which should you use?

  1. Reach for const first. Most variables are never reassigned. Making that explicit means a reader can stop tracking the value the moment they see the declaration.
  2. Switch to let only when you actually reassign — a loop counter, an accumulator, a value that changes across branches.
  3. Never use var in new code. There is no situation where it is the better choice. Its only remaining role is understanding legacy files.

The payoff is not stylistic. const by default makes reassignment stand out as a deliberate signal, block scoping keeps variables where you declared them, and the TDZ converts a class of silent undefined bugs into loud errors at the exact line responsible.

Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers