Write your own Promise.all()

7 min read
~1.3K words
22/02/24
19/03/23

Info

This post works in TypeScript and JavaScript. If you don't know, or don't care about TypeScript you can toggle it here , or in any of the code blocks in this post.

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>>)
  })
}
const myPromiseAll = promises => {
  return new Promise((resolve, reject) => {
    resolve([])
  })
}

Explanation of complex types

The types above may seem overly complex, but they're required to make sure types flow through the function. They make sure that the function's output is correctly typed and are exactly the same as the real Promise.all's types. Here's my best attempt at an explanation of the more confusing parts:

  • We need a generic so that the output types can match the input types. The Promises type argument extends readonly unknown[] to ensure that the input is an array. We mark the array type as readonly as it allows the function's types to work with a wider variety of inputs. The additional | [] allows the types of tuple elements to flow through the function too. Finally, we use unknown instead of any as it prevents us from accidentally making mistakes in the function body.
  • In the output type we use a mapped type which allows for correct output types when using tuples. The -readonly in the mapped type makes the resulting type not readonly, and Awaited unwraps promise types, converted something like Promise<number> to number.

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)
// ...
const results = []
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)
// ...
const results = []
 
for (const promise of promises) {
  promise.then(promiseResult => {
    results.push(promiseResult)
  })
}
 
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)
    }
  })
}
// ...
const results = []
 
for (const promise of promises) {
  promise.then(promiseResult => {
    results.push(promiseResult)
 
    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)
    }
  })
}
// ...
const results = Array(promises.length)
 
let numResolvedPromises = 0
 
for (let i = 0; i < promises.length; i++) {
  const promise = promises[i]
  promise.then(promiseResult => {
    results[i] = promiseResult
    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)
    })
}
// ...
const results = Array(promises.length)
 
let numResolvedPromises = 0
 
for (let i = 0; i < promises.length; i++) {
  const promise = promises[i]
  promise
    .then(promiseResult => {
      results[i] = promiseResult
      numResolvedPromises++
 
      if (numResolvedPromises === promises.length) {
        resolve(results)
      }
    })
    .catch(error => {
      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)
    })
}
// ...
const results = Array(promises.length)
 
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
      numResolvedPromises++
 
      if (numResolvedPromises === promises.length) {
        settled = true
        resolve(results)
      }
    })
    .catch(error => {
      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])
  // ...
}
// ...
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)
        })
    }
  })
}
const myPromiseAll = promises => {
  return new Promise((resolve, reject) => {
    const results = Array(promises.length)
 
    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
          numResolvedPromises++
 
          if (numResolvedPromises === promises.length) {
            settled = true
            resolve(results)
          }
        })
        .catch(error => {
          if (settled) return
 
          settled = true
          reject(error)
        })
    }
  })
}
Found a mistake, or want to suggest an improvement? Source on GitHub here
and see edit history here