DEV Community

Cover image for Rebuilding Promise.all()

Rebuilding Promise.all()

Drew Clements on June 07, 2021

I was presented with an interesting challenge recently. That challenge was to recreate a method provided by Javascript. Any guesses what that metho...
Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

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
𒎏Wii 🏳️‍⚧️

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
 
conorchinitz profile image
conorchinitz

Isn't that a problem for error-handling, though? If you're still awaiting the 999ms Promise and the 800ms one rejects with an error, what happens?

Collapse
 
drewclem profile image
Drew Clements

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

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

Siiiick!!