Write your own Promise.all()

7 min read
~1.3K words
NO DATE
03/19/23

Promise.all() is a super useful function that takes a list of promises and returns a promise that resolves to the results of each of the promises. Most importantly, it returns the results of your promises in the same order, and runs the promises concurrently.

depiction of Promise.all accepting a list of promises and return a promise resolving to the list with results in the order of the provided promises

Getting started

To get started, we need a function that matches the signature of Promise.all, i.e. one that accepts a list of promises and returns a promise. We're using the Promise constructor here as it allows us to manually resolve the promise whenever we want with its executor function (the function argument that you pass to new Promise). For now, we'll resolve the promise immediately with an empty array and for the majority of the article we'll be working out of the executor function.

typescriptjavascript
type PromiseAllResult<Promises extends readonly unknown[] | []> = Promise<{
-readonly [K in keyof Promises]: Awaited<Promises[K]>
}>
const myPromiseAll = <Promises extends readonly unknown[] | []>(
promises: Promises
): PromiseAllResult<Promises> => {
return new Promise((resolve, reject) => {
resolve([] as unknown as Awaited<PromiseAllResult<Promises>>)
})
}

Let's now extract out the results array so that we can easily set values in later steps:

typescriptjavascript
// ...
const results = [] as unknown as Awaited<PromiseAllResult<Promises>>
resolve(results)

The naïve approach

A first implementation might look like this:

typescriptjavascript
// ...
const results = [] as unknown as Awaited<PromiseAllResult<Promises>>
for (const promise of promises) {
promise.then(promiseResult => {
results.push(promiseResult as any)
})
}
resolve(results)

This has a very big problem however. Promises are asynchronous, so the function we pass to .then will not be called immediately and calling resolve right after the for-loop means the promise gets resolved with an empty array. In the demo below you probably won't even see the Loading... text due to how immediately myPromiseAll resolves.

Instead, we need some way of knowing when all the promises are complete and only then resolve-ing the promise.

Waiting for all the promises to finish

We can make sure all promises are complete by only resolve-ing our promise when the number of results we have equals the number of promises provided.

typescriptjavascript
// ...
const results = [] as unknown as Awaited<PromiseAllResult<Promises>>
for (const promise of promises) {
promise.then(promiseResult => {
results.push(promiseResult as any)
if (results.length === promises.length) {
resolve(results)
}
})
}

... and now this works! We're waiting for all the promises to complete before resolve-ing, so all the results are available myPromiseAll resolves.

However, we have one big problem: the results are sometimes in the wrong order:

Setting results in the right order

Making sure results end up in the right order requires a bit of a refactor - we'll need to:

  1. initialise the results array to be the same length as the input promises array.
  2. know the index of each promise so we can set its result in the correct position in the output array.
  3. separately keep track of how many promises have been resolved as point 1. means that results.length will always be the same as promises.length.
typescriptjavascript
// ...
const results = Array(promises.length) as unknown as Awaited<
PromiseAllResult<Promises>
>
let numResolvedPromises = 0
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise.then(promiseResult => {
results[i] = promiseResult as any
numResolvedPromises++
if (numResolvedPromises === promises.length) {
resolve(results)
}
})
}

We can now see that the order of results of our myPromiseAll function is consistently the same as Promise.all:

Handling errors

So far, we've handled everything going well, but we haven't handled any errors yet. Promise.all throws an error if any of the provided promises reject. We can accomplish this by calling reject with the error when any of the provided promises throw:

typescriptjavascript
// ...
const results = Array(promises.length) as unknown as Awaited<
PromiseAllResult<Promises>
>
let numResolvedPromises = 0
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise
.then(promiseResult => {
results[i] = promiseResult as any
numResolvedPromises++
if (numResolvedPromises === promises.length) {
resolve(results)
}
})
.catch((error: unknown) => {
reject(error)
})
}

Now, this is fine - the Promise constructor only accepts the first resolve, or reject, and any other times the functions are called are ignored. However, if you're like me, you'd like some extra confirmation that nothing goes wrong:

typescriptjavascript
// ...
const results = Array(promises.length) as unknown as Awaited<
PromiseAllResult<Promises>
>
let numResolvedPromises = 0
let settled = false
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise
.then(promiseResult => {
if (settled) return
results[i] = promiseResult as any
numResolvedPromises++
if (numResolvedPromises === promises.length) {
settled = true
resolve(results)
}
})
.catch((error: unknown) => {
if (settled) return
settled = true
reject(error)
})
}

Handling non-promises

We're now almost done. One last minor thing we need to do is handle when an element in the promises array isn't a promise. When Promise.all encounters a value that isn't a promise, it still works while our myPromiseAll so far would throw an error. We can fix this by wrapping each item in a promise that resolves immediately with Promise.resolve:

typescriptjavascript
// ...
for (let i = 0; i < promises.length; i++) {
const promise = Promise.resolve(promises[i])
// ...
}

All together

Here's the entire code for myPromiseAll:

typescriptjavascript
type PromiseAllResult<Promises extends readonly unknown[] | []> = Promise<{
-readonly [K in keyof Promises]: Awaited<Promises[K]>
}>
const myPromiseAll = <Promises extends readonly unknown[] | []>(
promises: Promises
): PromiseAllResult<Promises> => {
return new Promise((resolve, reject) => {
const results = Array(promises.length) as unknown as Awaited<
PromiseAllResult<Promises>
>
let numResolvedPromises = 0
let settled = false
for (let i = 0; i < promises.length; i++) {
const promise = Promise.resolve(promises[i])
promise
.then(promiseResult => {
if (settled) return
results[i] = promiseResult as any
numResolvedPromises++
if (numResolvedPromises === promises.length) {
settled = true
resolve(results)
}
})
.catch((error: unknown) => {
if (settled) return
settled = true
reject(error)
})
}
})
}
Found a mistake, or want to suggest an improvement? Edit on GitHub here
and see edit history here