<28/>
28 Lazy Coder
All

var, let, and const: The Difference That Actually Matters

Featured Image
The Article

I still remember the first time a var bug made me question my sanity. A loop, a setTimeout, and a variable that somehow had the same value in every callback instead of the value it had “at the time.” I genuinely thought the JavaScript engine was broken. It wasn’t. I just didn’t understand scope yet.

If you’ve been writing JavaScript for more than a few months, you’ve probably heard “just use const, and let when you need to reassign, and never use var” as a rule of thumb. It’s good advice. But it’s worth actually understanding why, because the reasoning behind it explains about half the “weird JavaScript” bugs you’ll ever run into.

The short version

That’s the summary you already knew. Here’s the part that actually matters day to day.

Scope is where the real difference lives

var doesn’t care about blocks — if, for, while — it only cares about functions. This means a var declared inside an if block is still visible outside it:

js

function example() {
  if (true) {
    var message = "hello";
  }
  console.log(message); // "hello" — leaked out of the block
}

let and const don’t do this. They respect the block they’re declared in:

js

function example() {
  if (true) {
    let message = "hello";
  }
  console.log(message); // ReferenceError: message is not defined
}

This isn’t a small stylistic difference — it’s the source of a whole category of bugs where a variable name gets accidentally reused two blocks away and silently overwrites something you didn’t mean to touch.

The classic loop bug

This is the one that actually convinced me to stop using var. Consider this:

js

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

Every callback logs 3, not 0, 1, 2 like you’d probably expect. Why? Because var i is function-scoped (or global, here), so there’s only one i shared across all three loop iterations. By the time the setTimeout callbacks actually run, the loop has already finished and i is 3.

Swap in let:

js

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

let creates a new binding of i for each iteration of the loop. Each callback closes over its own snapshot of i, not one shared variable. This single behavior change is, in my experience, the most common reason people’s “weird async loop bug” disappears the moment they switch var to let.

Hoisting: both are hoisted, but not the same way

Here’s a detail that trips people up: let and const are hoisted — contrary to what a lot of tutorials say — but they land in something called the “temporal dead zone” until the line where they’re actually declared.

js

console.log(a); // undefined — var is hoisted AND initialized to undefined
var a = 1;

console.log(b); // ReferenceError — let is hoisted but NOT initialized
let b = 2;

Practically, this means let/const fail loudly (a clear error) if you accidentally use them before declaration, while var fails silently (just gives you undefined and lets you keep going, possibly confused about why). Loud failures are a feature here, not a bug — they catch mistakes earlier.

const doesn’t mean “constant” the way you might expect

This one catches almost everyone at least once. const means the binding can’t be reassigned — it does not mean the value is frozen or immutable.

js

const user = { name: "Aditi" };
user.name = "Priya"; // totally fine
user = { name: "Priya" }; // TypeError — this is what const actually blocks

You can still mutate an object or push into an array declared with const — you just can’t point that variable name at a different object or array entirely. If you actually want immutability, Object.freeze() gets you closer (though it’s shallow — nested objects inside a frozen object aren’t automatically frozen too).

js

const config = Object.freeze({ retries: 3 });
config.retries = 5; // silently fails in non-strict mode, throws in strict mode

Redeclaration: var lets you shoot yourself in the foot

js

var count = 1;
var count = 2; // fine, no error, just quietly overwrites

js

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

In a small file this seems harmless. In a 400-line function somebody’s copy-pasted a block into twice, var will let you silently redeclare and reset a variable you didn’t mean to touch, with zero warning. let/const turn that into an error at write-time instead of a bug at runtime.

So when, if ever, should you use var?

Honestly? Close to never, in new code. The block-scoping behavior of let/const fixes real bugs, not just style preferences, and modern JavaScript (including every browser you’re likely to support today) handles both natively — there’s no compatibility reason left to reach for var. The only time I still see it is in older codebases that predate ES6, and even there, it’s usually worth migrating during a refactor rather than preserving.

My working rule, in order of preference:

  1. Reach for const by default. Most variables in a well-written function are never reassigned — they’re computed once and used.
  2. Use let only when you genuinely need to reassign — loop counters, accumulator variables, a value that changes based on a condition.
  3. Don’t use var. If you inherit code that uses it, that’s a good, low-risk refactor to make the first time you’re touching that file anyway.

Why this is worth actually understanding

None of this is trivia for its own sake. The loop-and-setTimeout bug above is a real bug that ships in real production code, often written by developers who “know” the var/let rule but never quite understood why it mattered — right up until it cost them an afternoon of debugging a closure that mysteriously always logged the same last value.

Understanding scope isn’t about memorizing a rule. It’s about knowing exactly what a variable is allowed to see, and when — which turns out to explain a surprising number of bugs that otherwise feel like JavaScript being “weird” for no reason.

AR

Ashutosh Rajbhar

Full-stack developer writing about clean code, frontend craft, and the occasional debugging war story.

Previous ← The Art of Effective Debugging Or How I Stopped Guessing