In JavaScript, we can use the async/await structure with the same logic as Promise chaining, but how exactly does the underlying execution model work? What roles do the event loop, microtask queue, and call stack play in these two approaches? Also, how does the `await` expression behave when running multiple Promises in parallel? In short, can you explain how these concepts are interconnected?
How does async/await and Promise chaining logic work in JavaScript?
👁️ 1 views💬 2 replies❤️ 0 likes
2 Replies
For the first time, I used async/await in a small project to aggregate data from multiple APIs while working on a dashboard that analyzes visitor statistics. Initially, I tried writing the sequence using chained Promises (`fetchA().then(...).then(fetchB)`), but soon noticed that the interface became slow because each request waited for the previous one to finish. When I rewrote the code using async with await, I stored all the Promises in variables before awaiting them:
```js
const p1 = fetch(url1);
const p2 = fetch(url2);
const p3 = fetch(url3);
const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
```
Here, the call stack remains attached to the async function until it hits the first await. When an await is executed, the function is paused in a "suspended" state, and the continuation is added to the micro-task queue, while the event loop continues processing other tasks and handling macro-tasks. If you use sequential awaits (`await fetchA(); await fetchB();`), each await adds a micro-task to the queue after the previous Promise resolves, effectively stepping through the function one operation at a time. However, when you use Promise.all or group multiple Promises before the await, all Promises execute in parallel (within the micro-task queue), and their results are received at once, significantly improving performance.
In short, async/await is just a clean syntactic wrapper around Promises that makes the code easier to read, while the underlying execution model remains the same: the call stack pauses at await, the event loop keeps running, and the micro-task queue handles the continuations of the Promises, whether they are sequential or parallel.
Async/await is essentially just syntactic sugar for Promises; when an `async` function is called, it immediately returns a Promise, and the code inside the function pauses execution at each `await`. Here’s what’s happening: the Promise being `await`ed is taken off the call stack until it resolves, and the remaining code is queued as a microtask. The event loop picks up the microtask from the microtask queue and pushes it back onto the call stack to continue execution.
Promise chaining (`p.then(...).then(...)`) works the same way—each `.then` callback is enqueued as a microtask. So, if a `.then` returns a value, it gets passed to the next `.then` in the same microtask cycle.
For parallel execution, `await` focuses on a single Promise. If you have multiple independent async operations, you combine them with `await Promise.all([p1, p2])` to start them simultaneously and wait for all to resolve before proceeding. Each Promise runs as its own microtask in the event loop, and `Promise.all` waits for the slowest one. If you write `await p1; await p2;`, the second operation waits for the first to finish, losing parallelism. In short, async/await and Promise chaining use the same async model—the only difference is syntax and readability. The microtask queue and call stack behave identically in both approaches.
Bottom line: if you structure your code with `Promise.all` for parallelism and use `await` to collect results, you get better performance and can handle errors in one place.