Skip to Content
UtilsPromises Closures Event Loop

🔄 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, and console.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 because await yields control to the event loop.
createAsyncCounter
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.

asyncCounter
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.

createTaskScheduler
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.

createTaskScheduler
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.

createCounter
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.

createAsyncMultiplier
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
Last updated on