Need help understanding JavaScript let vs var usage

I’m confused about when I should use let instead of var in my JavaScript code. I’ve seen different examples online, and some of my variables behave unexpectedly when I switch between them, especially inside loops and functions. Can someone clearly explain the real differences in scope and behavior, and share best practices for modern JavaScript so I don’t run into weird bugs?

Short version first:

Use let for almost everything.
Use const for values you never reassign.
Avoid var unless you work with old legacy code.

Now the why, with the stuff that bit you in loops.

  1. Scope rules

var is function scoped.
let is block scoped.

Example:

function test() {
if (true) {
var a = 1
let b = 2
}
console.log(a) // 1
console.log(b) // ReferenceError
}

a survives outside the if, b does not.
With let, your variables stay inside the block where you defined them.

  1. Loops and closures

This is likely what broke for you.

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

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

With var, there is one i shared by all callbacks.
By the time timeouts run, i is 3.

With let, each iteration gets its own j.
Callbacks see the value from that iteration.

So, if you have async code or callbacks inside loops, use let.

  1. Re-declaration

var allows re-declaration in the same scope.

var x = 1
var x = 2 // no error, silent overwrite

let blocks this.

let y = 1
let y = 2 // SyntaxError

This prevents accidental overwrites.

  1. Hoisting and the “temporal dead zone”

Both var and let are hoisted, but they behave different.

console.log(a) // undefined
var a = 10

console.log(b) // ReferenceError
let b = 10

With var, you get undefined, which often hides bugs.
With let, you get a clear error if you use a variable before its declaration.

  1. What to do in your code

Practical rules that work well:

• Use const by default.
• Change to let when you need to reassign.
• Avoid var unless you must support very old browsers without transpilation.

Example:

for (let i = 0; i < items.length; i++) {
const item = items[i]
// work with item
}

If you switch var to let inside loops and your code changes behavior, you relied on one of these:

• i leaking outside the loop
• i being shared across callbacks
• using the variable before its declaration

  1. Quick checklist

Use let when:
• You need a variable restricted to a block or loop.
• You use callbacks, async, promises inside loops.
• You want to avoid weird hoisting behavior.

Use var only when:
• You work in old code that already uses it.
• You know the function scoped behavior is what you need and you understand the side effects.

If you paste a small loop example that breaks when you change var to let, people can walk through the exact behavior step by step.

What @suenodelbosque wrote is solid, so I’ll try to hit different angles instead of repeating.

One big mental shortcut that helped me: var describes how JavaScript used to work, let describes how you probably think it works.

A few things that trip people up that weren’t mentioned directly:

  1. “I changed var to let and something broke”

That usually means at least one of these:

  • You were relying on the variable leaking outside a block:
if (true) {
  var msg = 'hi'
}
console.log(msg) // worked with var, dies with let

If your code “needs” that, you probably actually want to move the declaration outside:

let msg
if (true) {
  msg = 'hi'
}
console.log(msg)
  • Or you had code that used the variable before its declaration, and you didn’t notice because var quietly gave you undefined.

When you switch to let and get a ReferenceError, that’s not let being annoying, that’s it exposing a real bug.

  1. “But I want the loop variable after the loop!”

People sometimes do:

for (var i = 0; i < 10; i++) {}
console.log(i) // 10

Then they switch to let and get an error. The usual better pattern is: if you need the value after, declare it outside:

let i
for (i = 0; i < 10; i++) {}
console.log(i) // 10

You almost never need the loop’s var leak; it’s usually just a side effect that happened to “work.”

  1. Reassignment vs “new variable by accident”

One subtle advantage of let over var is catching accidental “new” variables inside the same block:

function fn() {
  var count = 1
  if (something) {
    var count = 2   // same variable, silently overwrites
  }
}

With let, if you actually meant “same variable,” then you just reassign:

let count = 1
if (something) {
  count = 2
}

If you accidentally write let count = 2 again, JS yells at you. That yelling is saving you from future debugging pain.

  1. Slight disagreement with the “avoid var unless legacy” rule

I mostly agree, but I’ll play devil’s advocate: in very low‑level performance‑critical code that you fully control, and where you understand function scoping deeply, using var consistently inside a small function is not “wrong.” It’s just old style.

That said, the benefit is basically zero these days compared to the cognitive overhead on everyone else reading the code. So in practice, yes, stick with let/const. I just wouldn’t treat var like black magic that must never be seen.

  1. How to train your brain

If your variables behave weirdly in loops & functions, do this:

  • Any variable that should only exist inside a loop body or if/try/catch block → let
  • Any value that you conceptually treat as a constant inside that block → const
  • Any variable you see from multiple blocks → define it once at the smallest common outer block and then assign to it inside.

Once you do that for a while, var will start to feel like that one ancient feature you only touch when editing old code and muttering under your breath.

If you post one of those “it worked with var, breaks with let” snippets, you’ll probably see that you were relying on hoisting, scope leaks, or a closure capturing the “one shared variable,” all of which let is intentionally making less surprising.

Let me zoom in on the “when should I actually pick let over var in real code?” angle, without rehashing what @viaggiatoresolare and @suenodelbosque already covered.

Think in terms of intent, not just rules:


1. What are you saying when you write var vs let?

  • var says:

    “This variable belongs to the whole function. It may get reused or overwritten anywhere inside it.”

  • let says:

    “This variable is only meaningful in this small block or loop.”

Most modern JavaScript is written with smaller, more focused scopes. So let better matches how we structure code today. Using var in a 2026 codebase is basically telling future readers: “Be careful, this might behave strangely around blocks, hoisting and loops.”

I slightly disagree with the idea that var is fine as long as you “understand it.” The problem is everyone else who will read your code. They might understand it too, but they now have to spend extra mental energy just verifying nothing weird is happening.


2. When var is actually a smell

A few specific patterns where var is a red flag:

  1. Shared loop counters

    for (var i = 0; i < 5; i++) {
      // ...
    }
    // More code...
    

    If i is var, you have to check every line after the loop to see if i is used. With let, you know the loop variable is done after the loop. Cleaner mental model.

  2. Utility functions in large files

    function calc() {
      var temp = heavyOperation()
      // 80 lines later...
      return temp * 2
    }
    

    If this function ever grows conditions, extra blocks, etc., it is very easy to accidentally reintroduce another var temp or rely on hoisting. With let, mistakes are caught earlier and more loudly.

  3. Multi-branch logic

    Any time you have if / else if / else, nested try / catch, or switch, var lets variables “leak” between branches in odd ways. let keeps each block more honest.


3. A simple mental rule that catches most tricky cases

When deciding between let and var, ask yourself:

“If I move this code into { }, should this variable still be visible or not?”

If the answer is “no” or “I’m not sure,” pick let and write it inside the smallest block possible.

If the answer is “yes, this is truly function-wide state,” you could use var, but even then let at the top of the function is clearer in modern style:

function process() {
  let total = 0

  if (condition) {
    total += 10
  }

  // more logic with total
}

This behaves like a function-scoped variable in practice, but without the older hoisting & redeclaration headaches.


4. When let exposes bugs you didn’t know you had

You mentioned “variables behave unexpectedly when I switch between them.” That usually means let is revealing:

  • Use-before-declaration bugs
  • Accidental reliance on leaked loop variables
  • Closures that captured a loop variable you thought was “frozen” per iteration

When that happens, do not “fix” it by going back to var. That just hides the bug again. Instead:

  • Move the declaration above the place it is used
  • Or narrow the scope and pass values into callbacks explicitly

Example rewrite:

for (let i = 0; i < arr.length; i++) {
  const value = arr[i]
  setTimeout(() => {
    console.log(value)
  }, 0)
}

Even if var “worked” here in your old code, this pattern makes intent painfully obvious.


5. About that unnamed “product title”

Since you mentioned the product title ``, here are some generic pros and cons as if we were evaluating a coding guideline or style resource that promotes let/const over var:

Pros:

  • Encourages clear scoping with let and const, which matches modern JS
  • Helps prevent subtle hoisting bugs and accidental redeclarations
  • Makes async and callback-heavy code more readable and predictable
  • Usually aligns with what linters and style guides already recommend

Cons:

  • Can feel strict or “annoying” at first when it surfaces old habits and hidden bugs
  • Might require refactoring if you have a lot of legacy var-heavy code
  • Developers familiar with only older JS might need a mindset shift

In practice, anything that systematizes “prefer const, then let, avoid var” tends to improve codebases over time, even if there is short-term friction.

Competitors like the approaches described by @viaggiatoresolare and @suenodelbosque are also solid: they focus on teaching semantics and mental models. I just lean a bit more on treating lingering var as a smell that should actively trigger a re-check of the design, not just “old but fine.”