跪拜 Guibai
← Back to the summary

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

Foreword

async/await is really pleasant to write — one await after another, asynchronous code reads like synchronous code.

But it falls apart when you have many interfaces: 5 interfaces awaited serially, and the user waits for the sum of those 5 time periods. Clearly, these 5 interfaces don't depend on each other, so why should they queue up?

The answer lies in Promise.all. Today, using a real, runnable piece of fetch dual-interface concurrent code, we'll string together the three states of a Promise, the "one fails, all fail" rule, and two-stage parallelism.

The Three States of a Promise: pending / fulfilled / rejected

As soon as new Promise executes, it immediately enters pending. It can then only proceed to one of two endpoints:

State Meaning Trigger Can it revert?
pending Pending, no result yet Just new'd Can transition
fulfilled Success resolve() Cannot revert to pending
rejected Failure reject() Cannot revert to pending, nor become fulfilled

Key rule: The state can only go from pending → fulfilled, or pending → rejected. Once settled, it cannot change. This is why a Promise is called a "promise" — it's either kept or broken, no take-backs.

In the console, the Promise {<pending>} you see is the external manifestation of this state machine. Once the result comes back, it either becomes <resolved> or <rejected>.

A Chain Reaction of Failure: One Rejection Spoils All

Promise.all has an iron rule: If any single Promise in the array is rejected, the whole thing is immediately rejected.

Furthermore, it doesn't wait for the other Promises to finish — the rest remain pending, and the result is discarded directly. What .catch gets is the reason for the first failure.

Scenario Promise.all behavior
All fulfilled Returns an array of results, order = input order
One rejected Overall rejected, goes to catch, gets the first failure reason
Multiple rejected Still goes to catch, only gives the first one
Some still pending No effect — it only cares if there is a reject, not progress

This rule dictates: The prerequisite for using Promise.all is "all requests must succeed." If even one interface fails, the entire parallel batch is wasted. If you can tolerate partial failure, you need to switch to Promise.allSettled.

Serial await vs Promise.all Parallelism

First, look at a real, runnable piece of code with two mutually independent interfaces:

js

const getStory = async () =>
  fetch('https://v1.hitokoto.cn/?c=i&encode=json');   // Hitokoto API
const getRatp = async () =>
  fetch('https://api.1314.cool/bingimg/?type=json&rand=1');  // Bing daily image

Serial approach (naive version):

js

async function main() {
  const story = await getStory();   // wait for hitokoto to return
  const ratp = await getRatp();     // then wait for bingimg to return
  // Total time ≈ T1 + T2
}

If each interface takes 500ms, serial execution takes 1000ms. The second interface could clearly run simultaneously with the first, but you've forced it to queue.

Parallel approach (Promise.all):

js

async function main() {
  const [storyRes, ratpRes] = await Promise.all([
    getStory(),
    getRatp(),
  ]);
  // Total time ≈ max(T1, T2)
}
Approach Total Time Result Order
Serial await T1 + T2 Code order
Promise.all max(T1, T2) Order of the input array

Key point: The result order of Promise.all has nothing to do with who finishes first — it collects results in the order of the input array you passed. bingimg returned before hitokoto? Doesn't matter, hitokoto is still first in the results array. This characteristic lets you confidently destructure [storyRes, ratpRes].

Nested Promise.all: Two-Stage Parallelism

Just making fetch concurrent isn't enough. fetch returns a Response object, and you need to call .json() to get the data — and .json() itself is also a Promise.

The approach in the notes is clever, the second stage is also parallel:

js

Promise.all([getStory(), getRatp()])
  .then(response => {
    return Promise.all(response.map(res => res.json()));
  })
  .then(([storyData, imgData]) => {
    console.log(storyData, imgData);
  })
  .catch(err => {
    console.log('One of them failed:', err);
  });

Breaking down the two stages of parallelism:

Stage What it does Parallel objects
First Promise.all Sends two fetch requests simultaneously [getStory(), getRatp()]
Second stage in .then Parses the json of both responses simultaneously response.map(res => res.json())

The line response.map(res => res.json()) is the essence — map converts each Response into the Promise res.json(), yielding an array of Promises, which is then fed to Promise.all.

Two stages of concurrent relay, with no point in the entire chain where it's "waiting dumbly." This is the correct way to use Promise.all.

5 Pitfall Reminders

1. Thinking res.json() inside .then is synchronous. It returns a Promise! Without await or wrapping it in Promise.all, what you get is a Promise object, not the data.

2. Writing serial await as "fake parallelism." await getStory(); await getRatp(); looks parallel but is actually serial — the two awaits are separated by waiting. For parallelism, you must use Promise.all, or first do const p1 = getStory(); const p2 = getRatp(); then await them separately.

3. Using the wrong method for interface fault tolerance. Promise.all fails entirely if one fails. If you want "successful ones get data, failed ones get a default value," switch to Promise.allSettled, which returns [{status, value/reason}, ...], and judge for yourself.

4. Mixing up result order. Don't assume "whoever returns first is placed first" — Promise.all strictly follows the input array order. If you want to process in order of completion, you need Promise.race or write it yourself.

5. Forgetting catch. If Promise.all rejects and you don't attach a .catch, the error is lost, and the console reports an UnhandledPromiseRejection. In a production environment, you must attach a catch, even if it's just for logging.

Afterword

Promise.all isn't just about being "a bit faster"; it's an expression of task dependency relationships — "this pile of things doesn't depend on each other, no one waits for anyone else." Serial await is "I absolutely must finish A before starting B," while parallel is "you all go together, I'll wait for the slowest one to return."