Implementing Promise.all, allSettled, race and any from scratch
Ever been in a Frontend interview where the interviewer asks you to explain all the Promise methods, in particular, .all, .allSettled, .race, .any? Sometimes they just ask for the definition. Sometimes, a code example of each one.
Let me give you some examples of what I typically do when I'm asked about them.
Promise.all
Nailing this one is a good sign, it's usually the first they ask. The definition itself is quite simple, but the implementation can get tricky.
Before diving into code, let's break it down:
Promise.alltakes an array of promises- Returns a single Promise that only resolves when all of them are resolved
- If any of the promises are rejected, the whole thing is rejected immediately as well (aka fail-fast)
- Keeps the original order of results, regardless of which Promise finished first
function promiseAll(promises) { return new Promise((resolve, reject) => { if (!Array.isArray(promises)) reject('Must be a valid array'); // Edge case: empty array resolves immediately if (promises.length === 0) resolve([]); const results = new Array(promises.length); let resolved = 0; promises.forEach((promise, index) => { Promise.resolve(promise).then(value => { results[index] = value; // important to use index to preserve the order (avoid using .push()) resolved++; if (resolved === promises.length) { resolve(results); } }).catch(reject); }); }); }A common mistake here is using .push() instead of index assignment. This matters because depending on whether the last promise is faster than the other ones, it will be return first, for example:
const p1 = Promise.resolve(1);const p2 = Promise.resolve(2);const p3 = Promise.resolve(3);
promiseAll([p1, p2, p3]).then(console.log); // [1, 2, 3]
// if promiseAll uses .push() it could return them in the wrong order: [3, 1, 2] instead of [1, 2, 3]Note: If any promise rejects,promiseAllwill callrejectimmediately via the.catchhandler.
Promise.allSettled
Kind of similar to Promise.all , the main differences are what is returned and the fact that it doesn't stop at the first rejection.It always waits for every promise to settle and returns each result as either fulfilled or rejected.
Let's take a look at it:
function promiseAllSettled(promises) { return new Promise((resolve, reject) => { if (!Array.isArray(promises)) reject('must be a valid array'); if (promises.length === 0) resolve([]); const results = new Array(promises.length); let settled = 0;
promises.forEach((promise, index) => { Promise.resolve(promise).then(value => { results[index] = { status: 'fulfilled', value, } }).catch(reason => { results[index] = { status: 'rejected', reason, } }).finally(() => { if (++settled === promises.length) resolve(results) }) }) })}If you test allSettled you will see something like this:
const p1 = Promise.resolve(1);const p2 = Promise.reject('oops!');const p3 = Promise.resolve(3);
promiseAllSettled([p1, p2, p3]).then(console.log);
// Output:// [// { status: 'fulfilled', value: 1 },// { status: 'rejected', reason: 'oops!' },// { status: 'fulfilled', value: 3 }// ]In summary the difference between .all and .allSettled is simple:
Promise.all— fails fast. One rejection and it's over.Promise.allSettled— waits for everyone. No matter what happens, it collects all results and resolves at the end. It never rejects.
Promise.race
Promise.race is definitely the simplest one to understand, no tricks, no deep logic. You just need to return the first finished promise, whether it resolves or rejects.
function promiseRace(promises) { return new Promise((resolve, reject) => { if (!Array.isArray(promises)) reject('must be a valid array');
promises.forEach(promise => { Promise.resolve(promise).then(resolve).catch(reject); }) })}That's it. Nothing else. It's kind of a race, whichever Promise finishes first wins, and that's the result you get. The rest are ignored. Simple enough, right?
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 1000));const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100));const medium = new Promise(resolve => setTimeout(() => resolve('medium'), 500));
promiseRace([slow, fast, medium]).then(console.log); // 'fast'
// Rejection wins if it's the fastestconst quickFail = new Promise((_, reject) => setTimeout(() => reject('oops'), 50));
promiseRace([slow, fast, quickFail]).catch(console.error); // 'oops'Promise.any
Last but not least: .any. Think of it as the opposite of .all. Where .all fails if anyone fails, .any only fails if everyone fails. Otherwise, it keeps going until it finds the first resolved one.
function promiseAny(promises) { return new Promise((resolve, reject) => { if (!Array.isArray(promises)) reject('must be a valid array'); if (promises.length === 0) resolve([]);
const errors = new Array(promises.length); let rejected = 0;
promises.forEach((promise, index) => { Promise.resolve(promise).then(resolve).catch(reason => { errors[index] = reason;
if (++rejected === promises.length) reject(errors); // you can also return errors using new AggregateError() }) }) })}So, finally, our output should be something like this:
const p1 = Promise.resolve(1);const p2 = Promise.resolve(2);const p3 = Promise.resolve(3);
promiseAny([p1, p2, p3]).then(console.log); // p1 => 1
const allPromises = [ Promise.reject('nope'), Promise.reject('nah'), Promise.reject('never'),];
promiseAny(allPromises).catch(err => { console.log(err); // ['nope', 'nah', 'never']});In short
all needs everyone to succeed. allSettled just needs everyone to finish. race takes whoever is fastest, whether it resolves or rejects. any takes the first win and only gives up when there's nothing left to win.