Using Promise.race to Access the Resolved Promise

Using JavaScript’s Promise.race method, we typically only retrieve the outcome (result or exception) of the first resolved or rejected Promise. However, sometimes it’s necessary to identify the specific Promise responsible for that resolution or rejection, similar to how Python’s asyncio.wait returns Futures. To achieve this, we can create a “wrapper” Promise.

This wrapper Promise acts as a stand-in, resolving or rejecting in sync with the original Promise but returning the original Promise itself instead of its result. While a tuple approach (combining the result and original Promise) was considered initially, it’s not strictly necessary. You can await the resolved original Promise again to retrieve its result.

Keep in mind that Promises don’t directly resolve to other Promises. A Promise (A) dependent on another Promise (B) waits for B to resolve to a value before A resolves to that same value. This behavior is illustrated in the following code snippets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
let result = await Promise.resolve(Promise.resolve("AA"));
console.log(result);
//AA

async function getMsg() {
    return Promise.resolve("AA");
}
result = await getMsg();
console.log(result);
//AA

result = await Promise.resolve("").then(result => Promise.resolve("AA"));
console.log(result);
//AA

result = await new Promise(resFn => resFn(Promise.resolve("AA")));
console.log(result);
//AA

To address this, we resolve the wrapper Promise to an array containing the original Promise. This prevents directly resolving to the original Promise’s result when awaiting the wrapper.

Let’s imagine an asynchronous function, getPost:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
async function asleep(timeout) {
    return new Promise(res => setTimeout(res, timeout));
}

const delays = {
    "A1": 1000,
    "B1": 2000,
    "C1": 50,
}

async function getPost(id) {
    console.log(`getting post: ${id}`)
    if (delays[id] === undefined) {
        await asleep(500);
        throw new Error(`Missing post: ${id}`);
    }

    await asleep(delays[id]);
    return `POST: [[${id}]]`;
}

We aim to execute multiple getPost actions concurrently and perform an action upon each post retrieval. Promise.race() is suitable for this, but we need to identify the resolved Promise to filter and rerun Promise.race() with remaining Promises, mirroring the behavior of Python’s asyncio.wait.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function runPromises1(postPromises) {
    while (postPromises.length) {
        // Notice how we wrap the Promise in an array. That way we have a Promise that resolves to an Array of a Promise
        // if we had a Promise p1 resolving to a Promise p2, then we would have the thing that p1 would not really resolve until p2 resolves, resolving to its result
        
        // this simple syntax works fine:
        let [pr] = await Promise.race(postPromises.map(p => p.then(result => [p]).catch(ex => [p])));
        try {
			// the "internal" pr Promise is already resolved/rejected at this point
            let result = await pr;
            console.log(`resolved index: ${pr._index}, result: ${result}`);
        }
        catch (ex) {
            console.log(`Error: resolved index: ${pr._index}, exception: ${ex}`);
        }        
        postPromises = postPromises.filter(p => p !== pr);
    }
}

async function main() {
    let postPromises = ["A1", "B1", "C1", "D1"].map(getPost); // (id => getPost(id));
    postPromises.forEach((pr, index) => pr._index = index);
    await runPromises1(postPromises);
}

main();

The crucial part lies in this line:
await Promise.race(postPromises.map(p => p.then(result => [p]).catch(ex => [p])))
Here, .then and .catch generate a new Promise resolving/rejecting to an array holding the original Promise.

Instead of then-catch, we can use an Immediately Invoked Async Arrow Function and leverage async. An async function returns a Promise that resolves or rejects upon function completion. Similar to before, we return the original Promise within an array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async function runPromises2(postPromises) {
    while (postPromises.length) {
        // Notice how we wrap the Promise in an array. That way we have a Promise that resolves to an Array of a Promise
        // if we had a Promise p1 resolving to a Promise p2, then we would have the thing that p1 would not really resolve until p2 resolves 
        
        // this more complex syntax also works fine, it's the same idea as the above
        // we have an Immediatelly Invoked Async Function Expression, it creates a Promise that resolves when the internal promise is resolved, returnig the promise itself (wrapped in an array)
        let [pr] = await Promise.race(postPromises.map(p => (async () => {
            try {
                await p;
            }
            catch {}
            return [p];
        })()));
        try {
            let result = await pr;
            console.log(`resolved index: ${pr._index}, result: ${result}`);
        }
        catch (ex) {
            console.log(`Error: resolved index: ${pr._index}, exception: ${ex}`);
        }        
        postPromises = postPromises.filter(p => p !== pr);
    }
}

In this scenario, using .then().catch() appears cleaner than async-await. Additionally, the need for the resolved Promise itself instead of its result might not be common in practice. Usually, passing a Promise to .race/.wait… that handles both invoking getPromise and the subsequent “print” action would suffice.

Licensed under CC BY-NC-SA 4.0
Last updated on Dec 03, 2022 15:05 +0100