DEV Community

loading...
Cover image for Rebuilding Promise.all()

Rebuilding Promise.all()

Drew Clements
Just a developer with more ideas and aspirations than time to explore them all!
・5 min read

I was presented with an interesting challenge recently. That challenge was to recreate a method provided by Javascript. Any guesses what that method was? That's right! Promise.all().

The purpose of the exercise was to get a deeper understanding of how promises in Javascript work. If you've worked with anything Promises or Async/Await before, then you know that there are specific things that have to happen in a certain way and order to work- and this is all by design.

Gathering Context

With that said, we can deduce a few things from Promise.all() and use that to scope what our function needs to do.

What do we know about Promise.all()? We know it accepts an array of promises. We know it returns an array of what those promises return. We know the returned array is itself a promise. We also know that it only does that last step if all promises succeed- so it errors if any of the passed in promises fail.

Our Definition of Done list could look something like this:

  • It accepts an array of promises
  • It returns a promise
  • It returns an array of those promises' returned value
  • It errors if any of the passed in promises fail

Jumping into the Code

We're only going to be writing a single function for this article. If you want to code along then you can pop open a new index.js file and use your terminal to test it once we're done.

Step one of this is to create the function. Since we're mimic-ing Promise.all(), we can call this function promiseAll

// index.js

function promiseAll() {

}
Enter fullscreen mode Exit fullscreen mode

Not too tough, huh? The next step is to let the function know to expect an array of promises when it is called.

// index.js

function promiseAll(promises) {

}
Enter fullscreen mode Exit fullscreen mode

That's Definition of Done number one off of our list!

  • It accepts an array of promises
  • It returns a promise
  • It returns an array of those promises' returned value
  • It errors if any of the passed in promises fail

Next we want to set this function up to return a promise.

I highlighted those two words because they almost literally tell us what we need to do next. How do you make a function return? How do you create a new Promise?

If you can answer those two questions, then you already know what our next code snippet should look like.

// index.js

function promiseAll(promises) {
  return new Promise((resolve, reject) => {

  })
}
Enter fullscreen mode Exit fullscreen mode

See what I mean? We had to return a new Promise. And that's item two of of our checklist

  • It accepts an array of promises
  • It returns a promise
  • It returns an array of those promises' returned value
  • It errors if any of the passed in promises fail

Returning an Array of Promises

Number 3 on our checklist is where the difficulty ramps up a bit.

Let's break down what we need.

We need:

  • an array we can return
  • to get the returned values of the promises passed in

Let's take that one step further. We know we're only going to return the promises' values in an array if they all return successfully.

Knowing that, lets create an array called successes

// index.js

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    let successes = [];
  })
}
Enter fullscreen mode Exit fullscreen mode

Now we need to somehow get the returned values of all promises passed in. Can you think of a couple of ways we can iterate through each promise?

We can use a for loop or the .map() method. Either here would work, but I'm going to use the .map() since I'm more familiar with it. Read up on .map() here

Let's map through our promises

// index.js

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    let successes = [];

    promises.map((promise) => {

    }
  })
}
Enter fullscreen mode Exit fullscreen mode

Now we can do promise stuff within the scope of each individual promise passed in.

What we'll be doing here is calling each promise individually and using a .then() to then get access to its returned value.

We'll also want to add a .catch to handle any errors. This actually checks off the fourth thing on our list.

// index.js

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    let successes = [];

    promises.map((promise) => {
      return promise.then((res) => {

      }.catch((err) => {
        reject(err)
      }
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

Remember that our larger function is trying to return an array of returned values. Knowing that, we shouldn't immediately resolve our promises.

Instead, we'll push our returned values to our successes array we created earlier.

// index.js

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    let successes = [];

    promises.map((promise) => {
      return promise.then((res) => {
        successes.push(res)
      }.catch((err) => {
        reject(err)
      }
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

We're getting close!!

Do you know what should happen next? Let's recap.

  • Our function is returning a promise.
  • Our function is set to error if any of the passed in promises fail.
  • And we're pushing our returned values to a successes array.

So what's left? Now we need to resolve our promise, but there's a condition with it.

We only want to resolve if all passed in promises succeed.

We can do that with an if statement by comparing the length of our successes array to the length of the promises passed in.

// index.js

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    let successes = [];

    promises.map((promise) => {
      return promise.then((res) => {
        successes.push(res)

        if(successes.length === promises.length) {
          resolve(successes)
        }
      }.catch((err) => {
        reject(err)
      }
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

Now we're making promises we can keep!

Congratulations! You've just rebuilt the functionality of Promise.all() and that's the last thing on our list!

  • It accepts an array of promises
  • It returns a promise
  • It returns an array of those promises' returned value
  • It errors if any of the passed in promises fail

Running our Promise

Finishing up, let's run our function and see how it works.

Add these to the bottom of your index.js file.

const p1 = Promise.resolve(1);
const p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 100));
promiseAll([p2, p1]).then((res) => console.log(res));
Enter fullscreen mode Exit fullscreen mode

Now, in your terminal-- be sure you're in the right directory- and run node index.js.

You should see the fruits of your labor console.logged before you!

Bonus

There's a small bug in this code. Can you see what it is?

Given the nature of promises, we can assume that there is a good chance that the promises passed in won't return in the same order as when they're passed in.

We're using .push() to add our returned value to the successes array. This means that values will always be inserted to the end of the array, so if promise 2 returns before promise 1, it will actually show up in the first index of the successes array.

You can actually see this in our example now.

You would expect to see [2, 1] since we passed the args in this order (P2, P1)- but they're actually backwards! What we see in the terminal is [1, 2].

This is because P1 resolves immediately, whereas p2 returns a new promise and then resolves.

How would you fix this?

Hint: You could use the index of each passed in promise and then insert their returned value at that index of the successes array.

Discussion (10)

Collapse
darkwiiplayer profile image
DarkWiiPlayer

After reading the introduction, I felt like doing this myself before continuing with the rest of the article; here's my result:

const all = arr => {
    return new Promise((final, reject) => {
        let c = 0
        const results = []
        const accept = x => {
            results.push(x)
            if (results.length >= arr.length)
                final(results)
        }
        arr.forEach(x => x.then(accept).catch(reject))
    })
}
Enter fullscreen mode Exit fullscreen mode

However, this looks very ugly, and keeping track of results manually feels very hacky. So here's a second iteration:

const all = async arr => {
    const results = []
    for (p of arr)
        results.push(await p)
    return results
}
Enter fullscreen mode Exit fullscreen mode
Collapse
lionelrowe profile image
lionel-rowe • Edited

This had me stumped for a while as to why the second iteration works — I was expecting the promises would run in serial so the time to completion would be longer. That isn't the case, because Promise.all takes an array of bare promises, not an array of functions resolving to promises.

With the following tuples of milliseconds and resolved values:

const tests = [
    [100, 0],
    [500, 1],
    [999, 2],
    [600, 3],
    [800, 4],
]
Enter fullscreen mode Exit fullscreen mode

Your second iteration runs as following:

  1. At ~100ms, push resolved value 0
  2. At ~500ms, push resolved value 1
  3. At ~999ms, push resolved values 2, 3, and 4 (because elements 3 and 4 have already resolved by this time)

Whether or not that's how Promise.all works under the hood, the end result is the same — all values, in the correct order, resolved in ~999 ms total time.

Console test runner
const start = Date.now()
const results = await promiseAll(tests.map(([ms, v]) => new Promise(res => setTimeout(() => res(v), ms))))
const end = Date.now()

console.log(JSON.stringify(results))
console.assert(JSON.stringify(results) === '[0,1,2,3,4]')

console.log(end - start)
console.assert(end - start >= 999 && end - start < 1050)
Enter fullscreen mode Exit fullscreen mode

Collapse
darkwiiplayer profile image
DarkWiiPlayer

Yes, this was quite unintuitive the first time I saw something like that, but you can just loop over threads, promises, etc. and wait for each one to finish, since you're ultimately waiting for the one that runs the longest anyway, and even if it comes first, the others will then just resolve instantly.

Thread Thread
lionelrowe profile image
lionel-rowe • Edited

I think what makes it doubly confusing is that a very common pattern for Promise.all is mapping over an array, so the callback to map takes a function resolving to a promise, even though what's directly being passed to Promise.all is the returned promises themselves.

const cb = url => fetch(url)
    .then(x => x.res())
    .then(x => x.json())

const promises = urls.map(cb)

const datas = await Promise.all(promises)
Enter fullscreen mode Exit fullscreen mode

cb is (url: String) => Promise<any>, but promises is Array<Promise<any>>, not Array<(url: String) => Promise<any>>.

To fetch and do something with each resolved object in series, you'd do this:

for (const url of urls) {
    const data = await cb(url)
    doSomething(data)
}
Enter fullscreen mode Exit fullscreen mode

Time taken: total latency of all requests.

But you could equally loop over the promises instead of the urls:

for await (const data of promises) {
    doSomething(data)
}
Enter fullscreen mode Exit fullscreen mode

Time taken: max latency of any one request.

Maybe this is just spelling out the obvious for some people but personally I still find it somewhat unintuitive, until you have that lightbulb moment 💡

Collapse
drewclem profile image
Drew Clements Author

I love this solution

Collapse
lionelrowe profile image
lionel-rowe • Edited

Bugfixed version:

Spoiler

We write directly to indexes in the array, instead of pushing, and keep track of the resolved count in a separate variable.

const promiseAll = promises => {
    return new Promise((resolve, reject) => {
        const values = new Array(promises.length)

        let resolvedCount = 0

        for (const [index, promise] of promises.entries()) {
            promise.then(value => {
                values[index] = value

                if (++resolvedCount === promises.length) {
                    resolve(values)
                }
            }).catch(err => reject(err))
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

Collapse
drewclem profile image
Drew Clements Author

Beautiful!!

Collapse
lionelrowe profile image
lionel-rowe

Can be made a little more elegant at the cost of a little performance like this:

const isDense = arr => arr.length === Object.keys(arr).length
// ...
if (isDense(values)) {
    resolve(values)
}
Enter fullscreen mode Exit fullscreen mode

That way we can get rid of the resolvedCount variable.

But I think @darkwiiplayer 's second solution is by far the most elegant 😉

Collapse
clandau profile image
Courtney

Nicely done!
here's what I did as I followed along:
pretty much the same except I used for - of

function promiseAll(promiseArray) {
    return new Promise((resolve, reject) => {
        let returnArray = []
        for (let prom of promiseArray) {
            prom.then((val) => {
                returnArray.push(val);
                if (returnArray.length === promiseArray.length){
                    resolve(returnArray)
                }
            })
            .catch(err => reject(err))
        }
    })     
}

Enter fullscreen mode Exit fullscreen mode
Collapse
drewclem profile image
Drew Clements Author

Siiiick!!