Leaderboard
Javascript Apr 7, 2026
Javascript Var Hoisting Closure

JavaScript Timers: What GetsLogged?

This is a daily Javascript challenge from the CodeShot archive. Practice your knowledge of Var Hoisting Closure and improve your technical interview readiness.

for(var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
A 0, 1, 2
B 3, 3, 3
C 0, 0, 0
D undefined, undefined, undefined

Detailed Explanation

Why This Question Matters

If you've ever spent an hour debugging a loop that was producing the exact same value over and over again, you've run into the classic JavaScript scope trap. This specific snippet is a rite of passage for JS developers. It looks dead simple—just a loop and a timer—but it reveals exactly how the JavaScript engine handles memory, variables, and the event loop.

The reason this trips people up is that it forces you to confront the difference between how we *think* code executes (line by line, top to bottom) and how it *actually* executes (asynchronous tasks waiting in a queue).

Understanding the Code

Let's look at the snippet:

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

At first glance, you might expect it to log 0, 1, 2. After all, the loop runs three times, and the setTimeout is set to 0 milliseconds. It seems like it should just fire off immediately.

But here is the catch: setTimeout is asynchronous. Even with a delay of 0, the callback function is pushed to the Task Queue. It won't actually execute until the main execution thread (the call stack) is completely empty.

By the time the first setTimeout callback finally runs, the for loop has already finished.

Now, let's talk about var. In JavaScript, var is function-scoped, not block-scoped. This means there is only one single instance of i for the entire loop. Every time the loop iterates, it's updating that same shared variable. By the time the loop ends, i has been incremented to 3.

Finding the Correct Answer

The correct answer is Option B: 3, 3, 3.

Here is the play-by-play of what's happening internally:

1. The loop starts. i is 0. A setTimeout is scheduled.
2. The loop continues. i becomes 1. Another setTimeout is scheduled.
3. The loop continues. i becomes 2. A third setTimeout is scheduled.
4. The loop increments i to 3. The condition i < 3 is now false. The loop exits.
5. Now the call stack is empty. The event loop looks at the Task Queue and sees three pending functions.
6. The first function runs: console.log(i). Since i is currently 3, it prints 3.
7. The second function runs: console.log(i). It still prints 3.
8. The third function runs: console.log(i). It prints 3 again.

If you wanted the output to be 0, 1, 2, you have two main options. The modern way is to swap var for let.

for(let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}

let creates a block scope. This means for every single iteration of the loop, JavaScript creates a new, unique binding for i. Each setTimeout callback captures its own version of i rather than sharing a global one.

Common Mistakes Developers Make

The biggest mistake is assuming that setTimeout(..., 0) means "run this right now." It doesn't. It means "run this as soon as the current script is finished and the stack is clear."

Another common pitfall is forgetting how var behaves. Many developers coming from C++ or Java expect loop variables to be contained within the curly braces {}. In JS, var ignores those braces entirely unless they are inside a function.

Some try to fix this using an Immediately Invoked Function Expression (IIFE), which was the standard "hack" before ES6:

for(var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(() => console.log(index), 0);
  })(i);
}

This works because the IIFE creates a new function scope for every iteration, effectively "locking in" the value of i at that moment. It's verbose, but it explains exactly what let is doing under the hood.

Real-World Usage

You won't often see people writing setTimeout inside a for loop with var in a professional codebase today, but the *concept* is everywhere.

This is exactly why you might see bugs in React when using useEffect or setTimeout inside a component. If you reference a state variable inside an async callback, you might be capturing a "stale" value from a previous render rather than the current one. This is known as a closure stale prop issue.

Understanding how closures capture variables by reference (like var) versus by value (essentially what happens with let in a loop) is the difference between a junior dev who guesses and a senior dev who knows why the bug is happening.

Key Takeaways

- var is function-scoped: It doesn't care about your for loop blocks; it's shared across the whole function.
- let is block-scoped: It creates a new variable for every iteration, which is almost always what you actually want.
- The Event Loop: Async functions (like setTimeout) always wait for the main synchronous code to finish before they execute.
- Closures: The callback "remembers" the environment where it was created. With var, that environment is a single shared variable that keeps changing.

Why this matters

Understanding Var Hoisting Closure is crucial for passing technical interviews. In real-world applications, this concept often leads to subtle bugs if not handled correctly. For more details, you can always refer to the official MDN Documentation.

📝
Reviewed by CodeShot Editorial
Every challenge is code-reviewed by senior developers to ensure accuracy and real-world relevance. Learn more.

Ready for your shot?

Join thousands of developers solving one logic puzzle every morning.

Solve Today's Challenge →