DEV Community

Discussion on: Rebuilding Promise.all()

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
 
drewclem profile image
Drew Clements

I love this solution

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?