The setTimeout Loop Closure Bug: var vs let
The most asked JS interview question. Here is exactly why var logs 3,3,3 and let logs 0,1,2.
The setTimeout Loop Closure Bug: var vs let
This is the single most asked JavaScript interview question. Here is exactly why var logs 3, 3, 3 and let logs 0, 1, 2.
The Code
// With var: for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // Output: 3, 3, 3 // With let: for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // Output: 0, 1, 2 `` ### Why `var` Logs `3, 3, 3` 1. `var i` is **function-scoped** (global here). There is **one** `i` for the entire loop. 2. Each iteration: - Checks `i < 3` (condition). - Schedules a callback via `setTimeout`. The callback is a closure over `i`. - Increments `i` (`i++`). 3. After the loop, `i` is 3 (the condition `3 < 3` fails). 4. The event loop runs the callbacks (after the stack is empty). 5. All three callbacks look up `i` the same `i`, which is now 3. 6. Output: `3, 3, 3`. Key point: closures close over the **variable**, not the **value**. All three callbacks reference the same `i`, and by the time they run, `i` is 3. ### Why `let` Logs `0, 1, 2` 1. `let i` is **block-scoped**. Each iteration gets a **fresh binding** of `i`. 2. Conceptually, the engine does something like: ```js // pseudocode let _i = 0; while (_i < 3) { let i = _i; // fresh copy for this iteration setTimeout(() => console.log(i), 0); _i++; } `` 3. Each callback closes over its own `i`, which holds the value for that iteration. 4. When the callbacks run, they each look up their own `i`: 0, 1, 2. 5. Output: `0, 1, 2`. ### The Fundamental Difference - `var`: **one** `i` shared by all iterations. Closures all see the final value. - `let`: **fresh** `i` per iteration. Each closure sees its own value. ### The IIFE Fix (Pre-ES6) ```js for (var i = 0; i < 3; i++) { (function (j) { setTimeout(() => console.log(j), 0); })(i); } `` The IIFE captures the current `i` as `j` in a new function scope. Each callback closes over its own `j`. ### The Takeaway `var` logs `3, 3, 3` because it is function-scoped (one shared `i`). `let` logs `0, 1, 2` because it is block-scoped (fresh `i` per iteration). Closures close over the variable, not the value. Fix with `let`, IIFE, or `setTimeout`'s extra arguments.
Because var is function-scoped. There is one i for the entire loop. All callbacks close over the same i. By the time they run (after the loop), i is 3. Closures close over the variable, not the value.
Because let is block-scoped and creates a fresh binding per iteration. Each callback closes over its own copy of i, which holds the value for that iteration. So when they run, each sees its own value.
The variable. A closure holds a reference to the variable, not a snapshot of its value. If the variable changes before the callback runs, the callback sees the updated value. This is why var in a loop logs the final value.
By creating a new function scope per iteration and capturing the current value of i as a parameter (j). Each callback closes over its own j, which is a snapshot of i at that iteration. This was the pre-ES6 workaround.
var creates one shared i for the entire loop; all closures see the final value. let creates a fresh i per iteration; each closure sees its own value. This is the key to the setTimeout loop closure bug.
Ready to master React completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master React
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

