setTimeout and Closures in JavaScript Explained
The setTimeout + closures loop question is the most famous JS interview question. Here is the full explanation.
setTimeout and Closures in JavaScript Explained
The setTimeout + closures + loop question is the most famous JavaScript interview question. It tests closures, var vs let, and how async callbacks work.
The Famous Question
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } `` **Output**: `3, 3, 3` ### Why? 1. `var i` is **function-scoped** (or global here). There is **one** `i` for the entire loop. 2. Each iteration schedules a callback via `setTimeout`. The callback is a closure that closes over `i`. 3. All three callbacks close over the **same** `i`. 4. `setTimeout` with delay 0 does not run immediately. The callbacks go to the macrotask queue. 5. The event loop runs them only after the synchronous loop finishes. By then, `i` is 3. 6. All three callbacks log 3. ### The Fix With `let` ```js for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } `` **Output**: `0, 1, 2` `let` is **block-scoped**. Each iteration gets a **fresh binding** of `i`. Each callback closes over its own `i`, which holds the value for that iteration. ### The Fix With IIFE (Pre-ES6) ```js for (var i = 0; i < 3; i++) { (function (j) { setTimeout(() => console.log(j), 0); })(i); } `` **Output**: `0, 1, 2` The IIFE creates a new function scope per iteration, capturing `i` as `j`. Each callback closes over its own `j`. ### The Fix With `setTimeout`'s Third Argument ```js for (var i = 0; i < 3; i++) { setTimeout((j) => console.log(j), 0, i); } `` **Output**: `0, 1, 2` `setTimeout` accepts extra arguments that are passed to the callback. Each call captures the current `i` as `j`. ### Variations Interviewers Ask #### Variation 1: Different Delays ```js for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), i * 1000); } `` **Output**: `3 (after 0s), 3 (after 1s), 3 (after 2s)`. The delay changes, but the value is still the shared `i = 3`. #### Variation 2: Using `let` With Different Delays ```js for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), i * 1000); } `` **Output**: `0 (after 0s), 1 (after 1s), 2 (after 2s)`. Each callback has its own `i`. ### The Takeaway The `setTimeout` + closures question tests: `var` is function-scoped (one `i` shared), `let` is block-scoped (fresh `i` per iteration), `setTimeout` callbacks run after the loop via the event loop, and closures close over the variable, not the value. Fix with `let`, IIFE, or `setTimeout`'s extra arguments.
3, 3, 3. var is function-scoped, so all callbacks share one i. By the time the callbacks run (after the loop, via the event loop), i is 3. All three callbacks log 3.
Use let instead of var (each iteration gets a fresh binding). Or use an IIFE to capture the current value of i as a parameter. Or pass i as an extra argument to setTimeout.
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. With var, all callbacks share one i.
No. The callback goes to the macrotask queue. The event loop runs it only after the current call stack is empty and all microtasks are done. So it runs after the synchronous loop finishes.
Yes. setTimeout(fn, delay, arg1, arg2, ...) passes the extra arguments to the callback. This can be used to capture the current value of i in a loop without let or an IIFE.
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.

