Closures in Async Code in JavaScript
Closures in JavaScript allow functions to remember their creation scope. When closures are combined with asynchronous code—like `setTimeout`, `async/await`, or `Promise`—unexpected situations can arise. In this article, we’ll explore how closures work in async code, what problems can occur, and how to solve them.
Closures and setTimeout in Loops
// Common mistake #1
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Output (after 1 second):
// 3
// 3
// 3
Explanation:
- What’s happening? The `setTimeout` callback is a closure that retains a reference to `i`.
- When the loop finishes, `i = 3`, and all closures see this final value.
// Solution: Use `let` (block-scoped `i`)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Output: 0, 1, 2
Alternative solution with IIFE (for `var`):
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(() => {
console.log(index);
}, 1000);
})(i);
}
Closures in Async/Await Functions
function createLogger() {
const username = "Aram";
return async function logMessage() {
await new Promise(res => setTimeout(res, 1000));
console.log("Hello", username);
};
}
const logger = createLogger();
logger(); // After 1 second => Hello Aram
What’s happening:
- `username` is preserved in the closure.
- Even though `await` pauses the function, the closure retains its lexical environment.
Closures and Promise Callbacks
function fetchData() {
const token = "secret-token-123";
return fetch("/api/data").then(() => {
console.log("Token used:", token);
});
}
fetchData();
Explanation:
- The `.then()` callback is a closure that retains `token`.
- This is very useful for passing auth headers or scoped configurations.
The Danger of Indices in Async Code
function delayedCounter() {
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log("Counter:", i), i * 500);
}
}
delayedCounter();
The problem (again, closure + `var`):
- All logs print the same value (`5`) because the closure retains the last reference to `i`.
Solution: Use `let` or clone the closure (IIFE):
// Simpler `let` version
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log("Counter:", i), i * 500);
}
Conclusion
Closures are a powerful tool for managing state in async code. However, if we use `var` or incorrectly pass variables, we may encounter unexpected results. Always remember: closures retain *references*, not values (for object-type variables).
Use `let` or IIFE for closures, be cautious with loops in async/await, and keep your closures clean and predictable.