🔄 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
await
vs. 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 becauseawait
yields 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: 3
Sequential 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 complete
Task 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: 3
Scheduling 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: 3
Preserving 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: 2
Asynchronous 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