# Why Your JavaScript Loop Logs the Same Value Every Time (And How to Fix It)


*Adapted from the [JavaScript Essentials Companion Guide](https://systemcraftpress.com/guides/javascript-essentials/?utm_source=crosspost&utm_medium=syndication&utm_campaign=javascript-loop-settimeout-same-value).*

---

You write a loop that should print 0, 1, and 2, one second apart. Instead you get three 3s.

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

Nothing crashed. No error. The values just aren't what the loop clearly seems to promise. The first instinct is usually to suspect `setTimeout` itself — some kind of timing quirk, a race condition, maybe the callbacks are firing out of order. None of that is what's happening.

## What's actually happening

`var` is function-scoped, not block-scoped. That means every single pass through the loop isn't creating a new `i` — there's exactly one `i` for the entire loop, shared by all three `setTimeout` callbacks. By the time any of those callbacks actually runs (a full second later, long after the loop has already finished all three iterations), `i` has already reached its final value: `3`. All three callbacks look up the same variable, at the same moment, after the loop is long done — so they all see the same thing.

This isn't a bug in `setTimeout`, and it isn't a race condition. The loop runs to completion essentially instantly; the callbacks are what's delayed, and they're all reading from a single shared box that's already empty of the values you wanted by the time they check it.

## The fix, step by step

1. **Recognize the signature**: a loop paired with a callback or `setTimeout`, where every output is identical — and it's always the *final* value the loop variable reached, never the first or the middle ones.
2. **Change `var` to `let`**:
   ```js
   for (let i = 0; i < 3; i++) {
     setTimeout(() => console.log(i), 1000);
   }
   // 0
   // 1
   // 2
   ```
3. **Understand why that one-word change works**: unlike `var`, `let` is block-scoped — it creates a *new* `i`, freshly bound, for every single iteration of the loop. Each `setTimeout` callback closes over its own separate `i`, not one shared variable, so each one remembers the value it was handed at that specific point in the loop.
4. **Confirm the fix conceptually, not just empirically** — if you're not sure why swapping the keyword fixed it, you'll hit the same shape of bug again the next time it shows up somewhere `let` isn't the obvious first move (a closure inside a function, for example, not just a loop).

## Two mistakes worth knowing about ahead of time

**Assuming it's a `setTimeout` or async timing problem.** This bug shows up identically with any deferred callback — event listeners, promises, array method callbacks assigned to run later — not just `setTimeout`. If you find yourself trying to "fix" it by reordering code or adding delays, that's a sign you're debugging the wrong layer. The loop already finished; nothing about timing changes what value is left behind.

**Reaching for the old IIFE trick out of habit, without knowing why `let` alone is now enough.** Before `let` existed, the standard fix was wrapping the loop body in an immediately-invoked function expression to force a new scope by hand. That still works, but it's solving a problem `let` already solves natively — if you're writing new code in 2026 and still reaching for an IIFE here, it's worth understanding that `let`'s per-iteration binding was specifically designed to make that pattern unnecessary.

## A debugging habit that works

Whenever a loop-plus-callback combination prints the same value repeatedly, don't start by investigating the callback's logic — check the loop variable's declaration first. `var` shared across every iteration versus `let` scoped fresh to each one explains this entire category of bug, and recognizing that signature immediately turns a confusing multi-minute debugging session into a one-word fix.

---

*If you'd like more posts like this sent straight to your inbox, [subscribe to the newsletter](https://buttondown.com/SystemCraftPress?utm_source=crosspost&utm_medium=syndication&utm_campaign=javascript-loop-settimeout-same-value).*

*Prefer to dig in yourself? The [JavaScript Essentials repo](hhttps://github.com/SystemCraftPress/javascript-essentials?utm_source=crosspost&utm_medium=syndication&utm_campaign=javascript-loop-settimeout-same-value) on GitHub has more free examples and exercises.*

