跪拜 Guibai
← All articles
Frontend

Stop Stacking Awaits: Two-Stage Parallelism with Promise.all

By 两只羊ovo ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Frontend performance is often bottlenecked by waterfall requests that could run in parallel. Replacing serial awaits with Promise.all is a low-effort, high-impact change, and the two-stage pattern eliminates the hidden serial cost of response parsing that many developers overlook.

Summary

A naive async/await chain fires one request, waits for it, then fires the next — total time is the sum of every round-trip. Promise.all dispatches all independent fetches at once, so the wall-clock cost drops to the slowest single request. The technique goes further: fetch returns a Response, and .json() is itself a Promise, so a second Promise.all over the response array parallelizes the parsing stage too. The result is a two-stage concurrent pipeline with no idle waiting between network I/O and data extraction. The piece also walks through the three Promise states, the all-or-nothing rejection rule, and five common mistakes including fake parallelism, forgotten catch handlers, and confusing result ordering.

Takeaways
Promise.all dispatches every fetch in the array simultaneously; total time becomes max(T1, T2) instead of T1 + T2.
Results from Promise.all are ordered by the input array, not by completion order, so destructuring is predictable.
A second Promise.all over response.map(res => res.json()) parallelizes JSON parsing, removing another serial bottleneck.
Promise.all fails fast: one rejection rejects the whole batch immediately, and only the first error is caught.
Use Promise.allSettled when partial failures are acceptable; it returns status and value/reason for each promise.
Forgetting .catch() on a Promise.all leaves unhandled rejections that surface as UnhandledPromiseRejection in production.
Calling res.json() without await or Promise.all returns a Promise object, not the parsed data.
Conclusions

The two-stage pattern is under-taught: most tutorials stop at parallel fetches and ignore that .json() is an async cost that can also be parallelized.

Promise.all's all-or-nothing semantics make it a poor default for user-facing UIs where one flaky endpoint shouldn't kill an entire page; allSettled is often the safer choice.

The 'fake parallelism' pitfall — writing two awaits back-to-back and assuming they run concurrently — is a common mental-model error that persists even among developers comfortable with async/await.

Concepts & terms
Promise states
A Promise is always in one of three states: pending (initial), fulfilled (resolved successfully), or rejected (failed). Once settled, the state cannot change.
Promise.all fail-fast
If any promise in the array rejects, Promise.all immediately rejects with that first error, regardless of whether other promises are still pending.
Promise.allSettled
Returns an array of results for every promise after all have settled, each with a status ('fulfilled' or 'rejected') and either a value or reason. Suitable when partial failures are acceptable.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗