Closures in async code in JavaScript
In this article, we will explore how closures work in async code.
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.
// Common mistake #1
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Output (after 1 second):
// 3
// 3
// 3
Explanation:
// 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);
}
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:
function fetchData() {
const token = "secret-token-123";
return fetch("/api/data").then(() => {
console.log("Token used:", token);
});
}
fetchData();
Explanation:
function delayedCounter() {
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log("Counter:", i), i * 500);
}
}
delayedCounter();
The problem (again, closure + `var`):
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);
}
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.