Build Your Own Promise.all (Step‑by‑Step Guide with a Polyfill)
When you need to run multiple asynchronous tasks in parallel and wait for all of them to finish, Promise.all is your go‑to. In this post, we’ll build a simple polyfill called Promise.myAll, explain how it works, cover edge cases, and test it with a few sample promises.
What Promise.all Does (in a nutshell)
- Takes an iterable of values (Promises or non-Promises).
- Resolves with an array of results when all inputs resolve.
- Rejects immediately if any input rejects (first rejection wins).
- Preserves the original order of results, regardless of completion times.
Baseline: Sample Promises
const prom1 = new Promise((resolve) => {
setTimeout(() => resolve("gfg1"), 1000);
});
const prom2 = new Promise((_, reject) => {
setTimeout(() => reject("error"), 2000);
});
const prom3 = new Promise((resolve) => {
setTimeout(() => resolve("gfg2"), 3000);
});
const prom4 = new Promise((resolve) => {
setTimeout(() => resolve("gfg3"), 3000);
});
First Attempt: A Working Promise.all Polyfill
Here’s a minimal implementation, close to what you shared, with clean formatting and comments:
Promise.myAll = function (values) {
return new Promise((resolve, reject) => {
// Handle empty array or non-array: normalize input
if (!values || values.length === 0) {
return resolve([]);
}
const results = new Array(values.length);
let settledCount = 0;
let done = false; // prevent multiple resolve/reject
values.forEach((item, index) => {
Promise.resolve(item)
.then((val) => {
if (done) return;
results[index] = val; // preserve order by index
settledCount += 1;
if (settledCount === values.length) {
done = true;
resolve(results);
}
})
.catch((err) => {
if (done) return;
done = true;
reject(err); // first rejection wins
});
});
});
};
What’s happening here
-
Promise.resolve(item)ensures we handle both Promises and plain values. - We store each resolved value by its original index to preserve order.
- We count how many have resolved; when all are done, resolve with the results array.
- If any rejects, we immediately reject and ignore future settlements (first rejection wins).
- We also handle empty input upfront by resolving to
[].
Why the empty array guard matters
In many naive implementations, passing [] would never resolve because the forEach never runs and the resolver is never called. The guard fixes that.
Testing Our Implementation
1) Case with a rejection
Promise.myAll([prom1, prom2, prom3])
.then((res) => {
console.log("Resolved:", res);
})
.catch((err) => {
console.log("Rejected:", err);
});
Expected output (after ~2s):
Rejected: error
2) All resolve
Promise.myAll([prom1, prom3, prom4])
.then((res) => {
console.log("Resolved:", res);
})
.catch((err) => {
console.log("Rejected:", err);
});
Expected output (after ~3s):
Resolved: [ 'gfg1', 'gfg2', 'gfg3' ]
3) Mixed with plain values (still works)
Promise.myAll([42, Promise.resolve("ok"), "plain"])
.then(console.log) // [42, 'ok', 'plain']
.catch(console.error);
Optional: Support Any Iterable (Not Just Arrays)
If you want to support any iterable and still preserve order:
Promise.myAll = function (iterable) {
return new Promise((resolve, reject) => {
const items = Array.from(iterable); // normalize
const len = items.length;
if (len === 0) return resolve([]);
const results = new Array(len);
let settledCount = 0;
let done = false;
for (let i = 0; i < len; i++) {
Promise.resolve(items[i])
.then((val) => {
if (done) return;
results[i] = val;
settledCount++;
if (settledCount === len) {
done = true;
resolve(results);
}
})
.catch((err) => {
if (done) return;
done = true;
reject(err);
});
}
});
};
Behavior Notes and Pitfalls
- Order is preserved: Results appear in the same order as inputs, not by completion time.
- Early rejection: The first rejection triggers the overall rejection. Other promises continue running in the background—JavaScript doesn’t cancel them automatically.
- Non-Promise values: They’re wrapped via
Promise.resolveand treated as already-resolved. - Empty input: Should resolve immediately to
[](guard included above). - Error handling: If you need to collect all outcomes regardless of errors, use
Promise.allSettledor implement a similar pattern.
Performance Snapshot
- Time: O(n) orchestrating promise settlements.
- Space: O(n) to store results.
Summary
- We implemented
Promise.myAllwith proper order preservation and early rejection. - We handled edge cases like empty inputs and non-Promise values.
- Use it as a learning tool; in production, prefer the native
Promise.allwhen available.
Top comments (0)