#Promises#Async/Await

Parallel vs Sequential Promises

Implement two async functions A and B, then return their sum using both sequential and parallel Promise execution, observing the time difference.

By Pratik RaiEasy

Running three requests one after another takes as long as all three combined. Running them together takes as long as the slowest one. Choosing wrong is one of the most common performance mistakes in async JavaScript — and one of the easiest to fix, because the two versions differ by about ten characters.

The difference in one example

Three requests, 200ms each:

JSfile.javascript
1// Sequential — 600ms 2const user = await fetchUser(id); 3const posts = await fetchPosts(id); 4const comments = await fetchComments(id); 5 6// Parallel — 200ms 7const [user, posts, comments] = await Promise.all([ 8 fetchUser(id), 9 fetchPosts(id), 10 fetchComments(id), 11]);

The sequential version waits for each response before starting the next request. Nothing about these three operations requires that — they are independent.

The rule

Await in a chain only when a later call needs an earlier result. Everything else should start together.

JSfile.javascript
1// Genuinely sequential — the second call needs the first's output 2const user = await fetchUser(id); 3const org = await fetchOrg(user.orgId); 4 5// Mixed — get the user first, then fan out 6const user = await fetchUser(id); 7const [org, posts] = await Promise.all([ 8 fetchOrg(user.orgId), 9 fetchPosts(user.id), 10]);

The dependency graph decides the shape. Draw it and the code writes itself.

await inside a loop

This is where sequential execution sneaks in unnoticed:

JSfile.javascript
1// Sequential — n × latency 2const results = []; 3for (const id of ids) { 4 results.push(await fetchItem(id)); 5} 6 7// Parallel — one round trip 8const results = await Promise.all(ids.map((id) => fetchItem(id)));

For 50 items at 100ms each, that is five seconds versus one hundred milliseconds. for...of with await is the single most common cause of an inexplicably slow page.

Note that forEach does not work here — it ignores the promises its callback returns, so execution continues before anything resolves:

JSfile.javascript
1ids.forEach(async (id) => { await fetchItem(id); }); // does not wait for anything

Promises start when created, not when awaited

The detail that makes everything above make sense. A promise begins executing the moment it is constructed. await only decides when you pause for the result.

JSfile.javascript
1const a = fetchA(); // already in flight 2const b = fetchB(); // also in flight — both running concurrently 3const resultA = await a; 4const resultB = await b; // likely already resolved

This runs in parallel despite two sequential await lines, because both promises were created before either was awaited. It is why Promise.all is not doing anything magical — the array literal starts every promise, and all just waits for them.

The corollary is a real trap: creating promises early and awaiting them much later means a rejection may occur before any handler is attached, producing an unhandled-rejection warning. Create and await in the same logical block.

When parallel is the wrong choice

Rate limits. Firing 500 requests simultaneously will hit an API's limit, get you a wall of 429s, and possibly a temporary ban. Full parallelism is only free when the count is bounded and small.

Resource exhaustion. Each in-flight request holds a socket and memory. Browsers cap concurrent connections per host at around six anyway, so beyond that you gain queueing, not speed. On a server, unbounded concurrency is a route to running out of file descriptors.

Ordered side effects. If each operation depends on the previous one having completed — writing sequential log entries, applying migrations — parallel is simply incorrect.

For a large list, the answer is neither fully sequential nor fully parallel but a concurrency pool:

JSfile.javascript
1async function pool(items, limit, worker) { 2 const results = new Array(items.length); 3 let cursor = 0; 4 5 const runners = Array.from({ length: Math.min(limit, items.length) }, async () => { 6 while (cursor < items.length) { 7 const index = cursor++; 8 results[index] = await worker(items[index], index); 9 } 10 }); 11 12 await Promise.all(runners); 13 return results; 14} 15 16const data = await pool(ids, 5, (id) => fetchItem(id));

A shared cursor keeps exactly limit operations in flight — as one finishes, that runner picks up the next index. This is what you want for anything above roughly ten items.

Error handling differs

With Promise.all, one rejection rejects the whole thing and you lose the successful results. If partial success is acceptable, use allSettled instead. And remember that neither cancels the operations still running — for that you need AbortController.

Key takeaways

  • Chain await only where a genuine data dependency exists.
  • await inside a for loop serialises everything — usually a bug.
  • forEach ignores async callbacks entirely.
  • Promises start on creation, not on await.
  • Unbounded parallelism trips rate limits; use a concurrency pool past ~10 items.
  • Promise.all discards successes on first failure — allSettled when partial results are useful.

Goal: Understand the difference between sequential and parallel Promise execution and how to use Promise.all to avoid async waterfalls.

Frequently asked questions

When should async work run sequentially rather than in parallel?
Only when a step genuinely needs the previous step's result. If the operations are independent, awaiting them one at a time turns a single wait into a queue of them — three 200ms requests become 600ms instead of 200ms.
What is the mistake that causes it?
`await` inside a `for` loop. It reads naturally and it is correct when there is a real dependency, which is why it survives review. `await Promise.all(items.map(fn))` starts everything at once and waits once, and the gap grows linearly with the list.
Does Promise.all have a downside?
Two. It rejects as soon as any promise rejects, discarding results that did succeed — `Promise.allSettled` is the answer when you want every outcome. And it starts everything simultaneously, which can overwhelm a server or hit a rate limit on a large list.
How do you run with limited concurrency?
Keep a pool of N in-flight promises and start a new one each time one settles. Interviewers like this because it is the point where the naive `Promise.all` answer stops being sufficient and you have to reason about scheduling yourself.

Related Challenges

Continue learning with these related challenges

View All
JavaScript

Fetch And Retry

Implement a retry function that handles transient failures by retrying async operations with optional delays.

JavaScript · Promises · Async/AwaitPratik Rai ·

JavaScript

Promise.all Polyfill

Implement your own Promise.all polyfill to understand how concurrent Promise handling works under the hood.

JavaScript · Promises · Async/AwaitPratik Rai ·

JavaScript

Promise.allSettled / race / any Polyfills

Implement the three Promise utility variants: allSettled (never rejects), race (first to settle wins), and any (first to resolve wins).

JavaScript · Promises · AsyncPratik Rai ·