JavaScript closure inside loops – simple practical example
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Closures inside loops are a classic JavaScript source of confusing output, especially in asynchronous code. The issue is not the closure itself, but which variable binding the closure captures. Once you understand how var, let, and function scopes interact, loop behavior becomes predictable.
Why the Classic var Example Fails
Consider delayed logging in a loop.
Many developers expect output 0, 1, 2, but they get 3, 3, 3.
Reason:
varis function-scoped, not block-scoped.- Each callback closes over the same
ibinding. - Loop finishes before callbacks execute, leaving
iequal to3.
So callbacks are correct according to captured binding, not intuitive iteration value.
Fix One: Use let in Loop Header
let creates a new binding per iteration in loop block scope.
Now output is 0, 1, 2 because each callback closes over a distinct iteration binding.
In modern JavaScript, this is usually the cleanest solution.
Fix Two: Capture Value via Function Argument
In older environments where let was unavailable, developers used function wrappers.
The wrapper receives current loop value and creates a new scope for each iteration.
Fix Three: Callback Parameters in Timers
Timer APIs let you pass extra callback arguments.
This avoids wrapper syntax and still preserves per-iteration values.
Practical UI Example with Event Handlers
Closure-loop issues appear often in DOM event binding.
All handlers log same final index in var version. Use let or local constant.
This is one of the most frequent real-world closure bugs in front-end code.
Async and Promise Loops
The same capture rule applies in async loops.
Again, output is final value repeated. Switching to let fixes it.
If asynchronous work depends on iteration values, always inspect binding strategy first.
Closure Is Useful, Not Just a Bug Source
Closures are fundamental and powerful when used intentionally.
Here closure preserves private state correctly. The loop issue comes from unintentional shared binding, not from closure concept itself.
Choosing Patterns in Production Code
Practical recommendations:
- prefer
letandconstfor new code - avoid
varin loop headers unless legacy compatibility requires it - keep callbacks small and test output explicitly
- use linter rules that disallow
varin modern codebases
Clear binding choices improve reliability more than clever callback tricks.
Debugging Strategy
When loop callbacks show wrong values, check these in order:
- whether loop variable uses
varorlet - whether callback executes later than loop completion
- whether closure captures mutable outer value
- whether value should be copied into local constant
This systematic check solves most closure-loop bugs quickly.
Common Pitfalls
A common pitfall is blaming timer delay order when the real issue is shared var binding. Another is mixing var and let in the same module and creating inconsistent closure behavior. Teams also often forget closure capture rules in event handlers and promises, then spend time debugging UI state that appears random. Finally, using complex wrapper patterns when simple let would be clearer reduces maintainability.
Summary
- Closure-loop bugs usually come from shared
varbindings. letcreates per-iteration bindings and is the preferred modern fix.- Wrapper functions and timer arguments are valid fallback patterns.
- The same capture rules apply to events, timers, and promises.
- Closures are powerful when used intentionally with clear binding scope.

