Suggest an editImprove this articleRefine the answer for “Що не так з var у циклі та setTimeout?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**var in a loop with setTimeout** prints the final loop value for all callbacks because `var` is function-scoped and all iterations share one variable. ```javascript for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 0, 1, 2 } ``` **Key point:** `let` creates a new binding per iteration. `var` shares one binding across all of them.Shown above the full answer for quick recall.Answer (EN)Image**var in a loop with setTimeout** - all callbacks print the final loop value, not the value from their iteration, because `var` is function-scoped and every iteration shares the same variable. ## Theory ### TL;DR - `var i` is one variable for the whole function. All three `setTimeout` callbacks point to it. - `setTimeout` is a macrotask. It runs after the loop finishes, when `i` is already `3`. - `let i` creates a new binding per iteration. Each callback gets its own copy. - Fix: replace `var` with `let`, or use an IIFE in ES5 code. ### Quick example ```javascript // Problem: var (prints 3, 3, 3) for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // all callbacks close over the same i } // Fix: let (prints 0, 1, 2) for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // each iteration has its own i } ``` `var i` gets hoisted to the function scope. By the time the callbacks fire, the loop is done and `i` is `3`. With `let i`, the engine creates a fresh binding for each iteration, so each callback captures a different value. ### Why var fails here The loop runs synchronously. JavaScript sends each `setTimeout` callback to the macrotask queue, and that queue is processed only after all synchronous code finishes. So all three callbacks run when `i` is already `3`. They all read the same variable and print the same value. The `0ms` version trips people up on interviews more than the `100ms` one, because zero delay feels like "right now". But `0ms` is not synchronous. The callback still waits in the queue. `let` solves this because the spec defines a fresh binding of `i` for each iteration. Each closure captures a different binding, not the same shared variable. That is the whole fix. Understanding [how the event loop works](/questions/event-loop) and [how closures capture variables](/questions/closure) makes this click faster. ### When to use what - Loop with `setTimeout`, Promises, or event listeners: use `let`. - ES5 codebase without `let`: IIFE with the value passed as an argument: `(function(i){ setTimeout(() => console.log(i), 100); })(i)`. - `forEach` or `.map()`: safe by default, each callback argument creates its own scope automatically. ### How the engine handles this V8 hoists `var i` to the top of the enclosing function, creating one Lexical Environment entry for the entire loop. `let i` triggers a "for loop block scope" mechanism from the spec: the engine creates a new Lexical Environment per iteration and copies the current binding value into it. This is also why [hoisting](/questions/hoisting) is worth knowing before this question comes up. ### Common mistakes **Mistake 1: assuming `setTimeout(..., 0)` runs during the loop** ```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // still 3, 3, 3 } ``` `0ms` delay does not mean synchronous. The callback still goes to the macrotask queue and runs after the loop finishes. **Mistake 2: IIFE without passing `i` as an argument** ```javascript // Wrong: still closes over the outer i for (var i = 0; i < 3; i++) { (function() { setTimeout(() => console.log(i), 100); // 3, 3, 3 })(); } // Correct: i becomes a local parameter for (var i = 0; i < 3; i++) { (function(i) { setTimeout(() => console.log(i), 100); // 0, 1, 2 })(i); } ``` The IIFE creates a new scope, but if you do not pass `i` as an argument, the inner function still reads the outer `i`. Passing it in creates a local copy at call time. **Mistake 3: trying `const` in a `for` loop** ```javascript for (const i = 0; i < 3; i++) {} // SyntaxError: Assignment to constant variable ``` `const` forbids reassignment. The `i++` step breaks immediately. `let` is the right tool here. ### Follow-up questions **Q:** Why does synchronous `console.log(i)` inside the loop print `0, 1, 2`, but `setTimeout` does not? **A:** Synchronous code runs inside the loop body, where `i` has the current iteration value. `setTimeout` defers execution to after the loop ends via the event loop, when `i` is already `3`. **Q:** What about `for...of` with `var`? **A:** Same issue. `for (var x of arr)` reassigns `x` each iteration. All callbacks close over the same `x` and read its final value. `for (let x of arr)` fixes it. **Q:** Two ways to fix this in ES5? **A:** First, IIFE with argument: `(function(i){ setTimeout(() => console.log(i), 100); })(i)`. Second, use `.forEach()`, which passes each element as a function argument and creates a new closure per call automatically. **Q:** Can `forEach` have the same bug? **A:** Yes, if you close over an external `var` instead of using the callback argument. `arr.forEach(function(){ setTimeout(() => console.log(externalVar), 100); })` reads `externalVar` at callback time, not at registration time. The callback argument is always safe. ## Examples ### Basic: the classic problem ```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Output: 3, 3, 3 for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Output: 0, 1, 2 ``` Three callbacks, one shared variable, and a queue that fires after the loop. That is the whole problem. `let` creates three separate bindings instead of one. ### Intermediate: `for...of` has the same trap ```javascript // var breaks for...of too for (var item of ['a', 'b', 'c']) { setTimeout(() => console.log(item), 0); } // Output: 'c', 'c', 'c' // let fixes it for (let item of ['a', 'b', 'c']) { setTimeout(() => console.log(item), 0); } // Output: 'a', 'b', 'c' ``` The same scoping rule applies to `for...of`. `var item` is hoisted out of the loop body, so all callbacks see the last assigned value. `let item` creates a fresh binding per iteration.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.