Clean way to wait for first true returned by Promise

You can implement that combining Promise.race and Promise.all:

function firstTrue(promises) {
    const newPromises = promises.map(p => new Promise(
        (resolve, reject) => p.then(v => v && resolve(true), reject)
    ));
    newPromises.push(Promise.all(promises).then(() => false));
    return Promise.race(newPromises);
}

Test for above code:

function firstTrue(promises) {
  const newPromises = promises.map(p => new Promise(
    (resolve, reject) => p.then(v => v && resolve(true), reject)
  ));
  newPromises.push(Promise.all(promises).then(() => false));
  return Promise.race(newPromises);
}

var test = values => firstTrue(
  values.map((v) => new Promise((resolve) => {
    setTimeout(() => resolve(v), Math.round(Math.random() * 1000));
  }))
).then((ret) => console.log(values, ret));

test([true, true, true]);
test([false, false, false]);
test([true, false, false]);
test([false, true, false]);
test([false, false, true]);

If you want the cutting edge solution, it sounds like you want Promise.any() which:

Promise.any() takes an iterable of Promise objects and, as soon as one of the promises in the iterable fulfils, returns a single promise that resolves with the value from that promise. If no promises in the iterable fulfil (if all of the given promises are rejected), then the returned promise is rejected with an AggregateError, a new subclass of Error that groups together individual errors. Essentially, this method is the opposite of Promise.all().

However, that isn't due to be usable until

  • Chrome 85
  • Firefox 79
  • Safari 14
  • No IE, Edge, or Opera support

You basically want some().

  • Promise.all() won't work because it will make you wait for all the Promises to complete.
  • Promise.race() alone won't work because it only returns the resolution of 1 Promise.

Instead, we need to receive an Array of Promises and keep racing them until one of them returns true at which point, we should stop. If none of them are true, we still need to stop, but we need to note that none of them were true.

Consider the following example with test harness:

Code

/**
 * Promise aware setTimeout based on Angulars method
 * @param {Number} delay How long to wait before resolving
 * @returns {Promise} A resolved Promise to signal the timeout is complete
 */
function $timeout(delay) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(), delay);
    });
}

/**
 * Return true (early) if any of the provided Promises are true
 * @param {Function(arg: *): Boolean} predicate The test the resolved Promise value must pass to be considered true
 * @param {Promise[]} arr The Promises to wait on
 * @returns {Promise<Boolean>} Whether one of the the provided Promises passed the predicate
 */
async function some(predicate, arr) {
    // Don't mutate arguemnts
    const arrCopy = arr.slice(0);

    // Wait until we run out of Promises
    while(arrCopy.length){
        // Give all our promises IDs so that we can remove them when they are done
        const arrWithIDs = arrCopy.map((p, idx) => p.then(data => ({idx, data})).catch(_err => ({idx, data: false})));
        // Wait for one of the Promises to resolve
        const soon = await Promise.race(arrWithIDs);
        // If it passes the test, we're done
        if(predicate(soon.data))return true;
        // Otherwise, remove that Promise and race again
        arrCopy.splice(soon.idx, 1);
    }
    // No Promises passed the test
    return false;
}

// Test harness
const tests = [
    function allTrue(){
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => true),
            $timeout(2000).then(() => true),
            $timeout(3000).then(() => true)
        ]).then(d => {
            console.log(d);
            console.log(new Date());
        });
    },
    function twoSecondsTrue(){
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => false),
            $timeout(2000).then(() => true),
            $timeout(3000).then(() => true)
        ]).then(d => {
            console.log(d);
            console.log(new Date());
        });
    },
    function threeSecondsTrue(){
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => false),
            $timeout(2000).then(() => false),
            $timeout(3000).then(() => true)
        ]).then(d => {
            console.log(d);
            console.log(new Date());
        });
    },
    function allFalse(){
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => false),
            $timeout(2000).then(() => false),
            $timeout(3000).then(() => false)
        ]).then(d => {
            console.log(d);
            console.log(new Date());
        });
    },
    function threeSecondsTrueWithError(){
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => { throw new Error() }),
            $timeout(2000).then(() => false),
            $timeout(3000).then(() => true)
        ]).then(d => {
            console.log(d);
            console.log(new Date());
        });
    }
]

tests.reduce((acc, curr) => acc.then(()=>curr()), Promise.resolve());

Output

// 1 Second true
2018-07-03T18:41:33.264Z
true
2018-07-03T18:41:34.272Z

// 2 Seconds true
2018-07-03T18:41:34.273Z
true
2018-07-03T18:41:36.274Z

// 3 Seconds true
2018-07-03T18:41:36.274Z
true
2018-07-03T18:41:39.277Z

// 3 Seconds false
2018-07-03T18:41:39.277Z
false
2018-07-03T18:41:42.282Z

// 3 Seconds true with error throwing
2018-07-03T18:41:42.282Z
true
2018-07-03T18:41:45.285Z

You can create a new promise that resolves as soon as any given promise resolves to true like this:

function promiseRaceTrue(promises) {
    return new Promise(function(resolve, reject) {
        promises.forEach(promise =>
            promise.then(val => val === true && resolve())
            // TODO handle resolve with value of "false"?
            // TODO handle rejection?
        );
        // TODO handle all resolved as "false"?
    });
}

var a = await promiseRaceTrue([Promise1(), Promise2(), Promise3()]);

It behaves similar to Promise.race but only resolves if one of the given promises resolves with the value true instead of resolving as soon as any of the given promises either resolves or rejects.