🔄 Promises, Closures and Event Loop
This section demonstrates how JavaScript promises, closures, and the event loop interact in real-world scenarios. These examples cover:
- How closures preserve state across async calls
- What happens when you
awaitvs. run multiple async calls in parallel - The importance of capturing variables in closures before
setTimeout - How the event loop schedules tasks with
setTimeout,Promise, andconsole.log - Sequential chaining of promises with closures
Perfect for understanding async behavior, state retention, and timing execution in complex JavaScript code.
Async function with closure and delayed execution
This example shows how closures capture state (count) and how await affects the execution order.
Each call to asyncCounter() increments count and logs it immediately. However, await delays further code inside the async function,
allowing subsequent calls to update count before the first finishes.
- Multiple calls to the same async function share the same closure state.
console.log("Script complete")runs immediately becauseawaityields control to the event loop.
function createAsyncCounter() {
let count = 0;
return async function incrementAsyncCounter() {
count++;
console.log({ count });
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Async Counter:", count);
};
}
const asyncCounter = createAsyncCounter();
asyncCounter();
asyncCounter().then(() => {
asyncCounter();
});
console.log("Script complete");
// { count: 1 }
// { count: 2 }
// Script complete
// 1 s delay
// Async Counter: 2
// Async Counter: 2
// { count: 3 }
// 1 s delay
// Async Counter: 3Sequential async calls with await and closure
This example demonstrates how sequential await calls work with closures. Each async call completes before the next one begins,
ensuring the counter increments correctly. The count variable is preserved across invocations due to the closure, and each setTimeout
resolves after 1 second, leading to predictable, step-by-step output.
function createAsyncCounter() {
let count = 0;
return async function incrementAsyncCounter() {
count++;
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Async Counter:", count);
};
}
const asyncCounter = createAsyncCounter();
await asyncCounter();
await asyncCounter();
await asyncCounter();
console.log("Script complete");
// with 1s between:
// Async Counter: 1
// Async Counter: 2
// Async Counter: 3
// immediately after counter 3:
// Script completeTask scheduling with closures and async timing
This example highlights how closure captures the shared taskCount variable, which is incremented before each scheduled task.
All tasks reference the same taskCount value by the time their setTimeout callbacks run, resulting in each log displaying Task Count: 3.
It also demonstrates how microtasks (like Promise.resolve().then) run before timers and the event loop order between synchronous code, promises, and setTimeout.
function createTaskScheduler() {
let taskCount = 0;
return function scheduleTask() {
taskCount++;
setTimeout(() => {
console.log("Task Count:", taskCount);
}, taskCount * 1000);
};
}
const scheduleTask = createTaskScheduler();
scheduleTask();
scheduleTask();
Promise.resolve().then(() => {
console.log("promise");
scheduleTask();
});
console.log("Tasks scheduled");
// Tasks scheduled
// promise
// after 1s with 1s between
// Task Count: 3
// Task Count: 3
// Task Count: 3Scheduling tasks with preserved state using closures
In this version, the closure captures the taskCount value in a separate variable savedCount before scheduling the setTimeout.
This ensures that each task logs the correct value at the time it was created, rather than referencing the final shared taskCount.
The result is that each setTimeout logs a different value in increasing order, showcasing how to avoid timing issues by saving state early.
function createTaskScheduler() {
let taskCount = 0;
return function scheduleTask() {
taskCount++;
const savedCount = taskCount;
setTimeout(() => {
console.log("Task Count:", savedCount);
}, taskCount * 1000);
};
}
const scheduleTask = createTaskScheduler();
scheduleTask();
scheduleTask();
Promise.resolve().then(() => {
scheduleTask();
});
console.log("Tasks scheduled");
// Tasks scheduled
// after 1s with 1s between:
// Task Count: 1
// Task Count: 2
// Task Count: 3Preserving async state with closure in chained promises
This example demonstrates how closures can maintain and update state (count) across multiple asynchronous calls.
Because the promise is chained (then -> then), each counter() call executes sequentially, allowing count to increment between calls.
The use of setTimeout inside the promise simulates asynchronous work, and the closure ensures the count value is correctly preserved across executions.
function createCounter() {
let count = 0;
return function incrementCounter() {
count++;
return new Promise((resolve) => {
setTimeout(() => {
resolve(count);
}, 1000);
});
};
}
const counter = createCounter();
// if it was called at the same time, count would
// have not been saved in closure
counter()
.then((result) => {
console.log("Counter 1:", result);
return counter();
})
.then((result) => {
console.log("Counter 2:", result);
});
console.log("Script in progress");
// Script in progress
// after 1s with 1s between:
// Counter 1: 1
// Counter 2: 2Asynchronous multiplier using closure and chained promises
This example showcases how closures can maintain internal state (factor = 2) in an asynchronous function.
The multiplyByTwo function, returned by createAsyncMultiplier, remembers the factor across multiple chained .then() calls.
Each multiplication is delayed with setTimeout, and the result of one multiplication is passed to the next, demonstrating sequential asynchronous logic with shared state.
function createAsyncMultiplier() {
let factor = 2;
return function multiply(value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value * factor);
}, 1000);
});
};
}
const multiplyByTwo = createAsyncMultiplier();
multiplyByTwo(5)
.then((result) => {
console.log("Multiply 1:", result);
return multiplyByTwo(result);
})
.then((result) => {
console.log("Multiply 2:", result);
return multiplyByTwo(result);
})
.then((result) => {
console.log("Multiply 3:", result);
});
console.log("Multiplication started");
// Multiplication started
// after 1s with 1s between:
// Multiply 1: 10
// Multiply 2: 20
// Multiply 3: 40