31

I'm puzzled by something in the ES6 Promise API. I can see a clear use case for submitting multiple async jobs concurrently, and "resolving" on the first success. This would, for example, serve a situation where multiple equivalent servers are available, but some are perhaps down, and others heavily loaded and slow, so my goal would be to get a response from the first one to succeed, and ignore the rest (yes, I know this is an obnoxious way for a client to behave from a server's perspective, but it's great for the end user ;)

However, as far as I can see, I have either "all" or "race" behaviors to play with. The "all" behavior seems to wait until all the requests have completed, which means that I have to wait for the slowest, even if a server has already completed (indeed, I might have to wait for a timeout, with would be a disaster for this scenario.) The "race" behavior, however, seems to give me the first to complete, which if that happens to be a failure, is also a disaster.

Is there something in the API that permits a "raceToSuccess" kind of behavior, or do I have to build it by hand. For that matter, how would I build it by hand?

As a side note, I found the same puzzle in the Java 8 CompletableFuture, which seems to be a closely parallel API. So, am I missing something at a philosophical level?

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
Toby Eggitt
  • 1,806
  • 19
  • 24

7 Answers7

68

This is a classic example where inverting your logic makes it much clearer. Your "race" in this case is that you want your rejection behavior to in fact be success behavior.

function oneSuccess(promises){
  return Promise.all(promises.map(p => {
    // If a request fails, count that as a resolution so it will keep
    // waiting for other possible successes. If a request succeeds,
    // treat it as a rejection so Promise.all immediately bails out.
    return p.then(
      val => Promise.reject(val),
      err => Promise.resolve(err)
    );
  })).then(
    // If '.all' resolved, we've just got an array of errors.
    errors => Promise.reject(errors),
    // If '.all' rejected, we've got the result we wanted.
    val => Promise.resolve(val)
  );
}
loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • 2
    Plus one for avoiding the promise constructor anti-pattern. – jib May 15 '16 at 13:33
  • 2
    Yeah, it's one of those things I look out for when promise questions pop up. If you're using `new Promise` for anything other than converting a callback API to a promise API, you need a _really_ good reason. – loganfsmyth May 15 '16 at 19:00
  • 3
    Gach, so simple when you put it like that. Thanks for a very elegant solution. I admit I still can't help thinking it belongs in the API! – Toby Eggitt May 16 '16 at 01:06
  • 2
    `Promise.all` and `Promise.race` are essentially `AND` and `OR`, so you can build any logic you'd need when combined with negation like this. It's hard to know when to draw a line when deciding what to include in an API, but these are at least solid building blocks. – loganfsmyth May 16 '16 at 03:40
  • 1
    Nice solution! Bergi pointed me to this, and I used the idea as part of an answer to [another question](http://stackoverflow.com/a/42046463/5459839). Note that you can do `err => err` instead of `err => Promise.resolve(err)`, and also `val => Promise.reject(val)` can be written as `val => {throw val}` – trincot Feb 06 '17 at 18:59
  • 1
    Yeah I think it's all preferences. I added them explicitly to keep things a little more readable for the symmetry between the two. I also personally don't love `throw` since it's more likely to get caught by devtools when it is set to pause on all exceptions. – loganfsmyth Feb 07 '17 at 00:22
  • Shouldn't line 10 first parentheses be a closing curly brace, and a line 15 have an additional closing parentheses? Would edit but not 100% sure and requires at least 6 characters (and I only think 2 chars need edit). – PotatoFarmer Jan 18 '19 at 14:57
  • @PotatoFarmer Thanks! – loganfsmyth Jan 18 '19 at 16:26
  • 3
    +1 for thinking outside the box (inverting the logic) but also this seems quite hacky and **‘unsemantic’**! Turning resolutions into rejections and vice versa… it gets the job done, but wow does it fck with my mind. Nicely done. – chharvey Jan 22 '19 at 04:00
9

You can write this quite easily yourself.

function raceToSuccess(promises) {
  return new Promise(
    resolve => 
      promises.forEach(
        promise => 
          promise.then(resolve)
      )
  );
}

This kicks off all the promises, and when any succeeds resolves the new promise with its value. Failed promises are ignored. Subsequent successful promises cause nothing to happen, since the new promise has already been resolved. Note that the resulting promise will never resolve or reject if none of the input promises resolve.

Here is a modified version which returns a rejected promise if all of the input promises reject:

function raceToSuccess(promises) {
  let numRejected = 0;

  return new Promise(
    (resolve, reject) => 
      promises.forEach(
        promise => 
          promise . 
            then(resolve) .
            catch(
              () => {
                if (++numRejected === promises.length) reject(); 
              }
           )
       )
  );
}

I like @loganfsmyth's approach; you should probably upvote it for its conceptual clarity. Here's a variation of it:

function invertPromise(promise) {
  return new Promise(
    (resolve, reject) => 
      promise.then(reject, resolve)
  );
}

function raceToSuccess(promises) {
  return invertPromise(
    Promise.all(
      promises.map(invertPromise)));
}

Another idea is to turn the failed promises into promises which neither resolve nor reject (in other words, are permanently pending), then use Promise.race:

function pendingPromise()      { return new Promise(() => { }); }
function killRejected(promise) { return promise.catch(pendingPromise); }

function raceToSuccess(promises) {
  return Promise.race(promises.map(killRejected));
}

You may or not like the behavior of this. The returned promise will never fulfill or reject if none of the input promises fulfill. It's also possible that the permanently pending promises will not get GC'd, or some engines might eventually complain about them.

  • Useful discussions, thanks for adding these. As you note, I like the clean simplicity of @loganfsmyth's answer, but yours definitely adds value. – Toby Eggitt May 16 '16 at 01:08
4

I'm using a function based on Promise.race() but with a twist: it ignores rejects, unless all given promises reject:

// ignores any rejects except if all promises rejects
Promise.firstResolve = function (promises) {
    return new Promise(function (fulfil, reject) {
        var rejectCount = 0;
        promises.forEach(function (promise) {
            promise.then(fulfil, () => {
                rejectCount++;
                if(rejectCount == promises.length) {
                    reject('All promises were rejected');
                } 
            });
        });
    });
};

It's based on Rich Harris's Promise polyfill race method. I just made the looping promise reject conditional: it only rejects the main promise, if all given promises failed, otherwise it ignores rejects and resolves the first success.

Usage:

// fastest promise to end, but is a reject (gets ignored)
var promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("foo")
    }, 100);
})

// fastest promise to resolve (wins the race)
var promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("bar")
    }, 200);
})

// Another, slower resolve (gets ignored)
var promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("baz")
    }, 300);
})

Promise.firstResolve([promise1, promise2, promise3])
    .then((res) => {
        console.log(res) // "bar"
    })
    .catch(err => {
        console.log(err) // "All promises were rejected" (if all promises were to fail)
    })

The reason I use this instead of the promise inverting approach, is because in my opinion this is more readable.

To please the question in the strictest way, below there is a version that resolves the first successful promise but doesn't do anything if all given promises fail:

// ignores any and all rejects
Promise.firstResolve = function (promises) {
    return new Promise(function (fulfil) {
        promises.forEach(function (promise) {
            promise.then(fulfil, () => {});
        });
    });
};

(usage same as above)

Edit: This is in fact the same as @user663031's suggestion. Which I haven't realized until just now.

Adam Karacsony
  • 151
  • 1
  • 9
4

Is there something in the API that permits a "raceToSuccess" kind of behavior

There is now. There is a finished Stage 4 proposal for Promise.any:

Promise.any() takes an iterable of Promise objects and, as soon as one of the promises in the iterable fulfills, returns a single promise that resolves with the value from that promise.

So, the following syntax can be used:

// assume getApi returns a Promise

const promises = [
  getApi('url1'),
  getApi('url2'),
  getApi('url3'),
  getApi('url4'),
];
Promise.any(promises)
  .then((result) => {
    // result will contain the resolve value of the first Promise to resolve
  })
  .catch((err) => {
    // Every Promise rejected
  });

Promise.any has been implemented in all modern browsers. There are some polyfills available too.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
1

Old topic but here's my entry; it's essentially @loganfsmyth's solution, but with a few more checks to conform to conventions established by Promise.all():

  • Empty array as input returns (synchronously) an already resolved promise
  • Non-promise entries in the array results in the 1st such entry to be used as the resolved value

Promise.any = a => {
  return !a.length ?
    Promise.resolve() :
    Promise.all(a.map(
      e => (typeof e.then !== 'function') ?
        Promise.reject(e) :
        e.then(
          result => Promise.reject(result),
          failure => Promise.resolve(failure)
        )
    )).then(
      allRejected => Promise.reject(allRejected),
      firstResolved => Promise.resolve(firstResolved)
    );
};

// Testing...

function delayed(timeout, result, rejected) {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => rejected ? reject(result) : resolve(result),
      timeout);
  });
}

Promise.any([
  delayed(800, 'a'),
  delayed(500, 'b'),
  delayed(250, 'c', true)
]).then(e => {
  console.log('First resolved (expecting b):', e);
});

Promise.any([
  delayed(800, 'a', true),
  delayed(500, 'b', true),
  delayed(250, 'c', true)
]).then(null, e => {
  console.log('All rejected (expecting array of failures):', e);
});

Promise.any([
  delayed(800, 'a'),
  delayed(500, 'b'),
  delayed(250, 'c', true),
  'd',
  'e'
]).then(e => {
  console.log('First non-promise (expecting d):', e);
});

// Because this is the only case to resolve synchronously,
// its output should appear before the others
Promise.any([]).then(e => {
  console.log('Empty input (expecting undefined):', e);
});
tavnab
  • 2,594
  • 1
  • 19
  • 26
0

I expanded the @loganfsmyth approach with timeouts and I wrote a small function that:

  • runs all the promises,
  • wait for the promises to succeed for no more than a specified amount of time (options.timeOutMs),
  • return the first that succeed.

In the following snippet, you can test it:

const firstThatCompleteSuccessfullyES6 = (options) => {

    // return the first promise that resolve
    const oneSuccess = (promises) => Promise.all(promises.map(p => {
                    // If a request fails, count that as a resolution so it will keep
                    // waiting for other possible successes. If a request succeeds,
                    // treat it as a rejection so Promise.all immediately bails out.
                    return p.then(
                        (val) => { return Promise.reject(val); },
                        (err) => { return Promise.resolve(err); }
                    );
            })
            ).then(
                // If '.all' resolved, we've just got an array of errors.
                (errors) => { return Promise.reject(errors); },

                // If '.all' rejected, we've got the result we wanted.
                (val) => { return Promise.resolve(val); }
            );
    

    // return the promise or reect it if timeout occur first
    const timeoutPromise = (ms, promise) => new Promise(function(resolve, reject) {
            setTimeout(() => reject(new Error('timeout')), ms);
            promise.then(resolve, reject);
        });
    

    if (options.subsystems.length < 1) {
        return Promise.reject('Parameters error, no subSystems specified');
    }

    const timedOutSubsystems = options.subsystems.map(function(subsystem){
        return timeoutPromise(options.timeOutMs, subsystem(options));
    });

    const startDate = Date.now();

    return oneSuccess(
        timedOutSubsystems
    )
    .then((result) => {
        const elapsedTime = Math.abs((startDate - Date.now()) / 1000);
        console.log('firstThatCompleteSuccessfully() done, after s: ' + elapsedTime + ': '+ result);
        return result;
    })
    .catch((error) => {
        const elapsedTime = Math.abs((startDate - Date.now()) / 1000);
        console.error('firstThatCompleteSuccessfully() error/nodata: ' + error);
    });

}



// example of use with two promises (subsystem1 & subsystem2) that resolves after a fixed amount of time

const subsystem1 = (options) => new Promise(function(resolve, reject) {
        setTimeout(function(){
            console.log('subsystem1 finished');
            resolve('subsystem 1 OK');
        }, 1000);
    });



const subsystem2 = (options) => new Promise(function(resolve, reject) {
        setTimeout(function(){
            console.log('subsystem2 finished');
            resolve('subsystem 2 OK');
        }, 2000);
    });


firstThatCompleteSuccessfullyES6({
    subsystems: [subsystem1, subsystem2],
    timeOutMs: 2000
})
.then((result) => console.log("Finished: "+result));
mircoc
  • 159
  • 2
  • 8
0

To resolve this problem i used a Promise.rice with a Promise.allSettled.

The next code wait with the Promise.rice a success value. but if no hay a success result. return a array with all errors.

const PromiseRiceSuccess = <T = unknown>(promises: Promise<T>[]) => {
  let done: (reason?: T) => void;
  const waitEndAllPromises = new Promise((resolve, reject) => done = reject);
  const waitCatchs = promise => Promise.resolve(promise).catch(() => waitEndAllPromises);

  Promise.allSettled(promises).then(r => done(r));

  return Promise.race(promises.map(waitCatchs));
};

Example:

PromiseRiceSuccess([
  Promise.reject(1),
  new Promise((r) => setTimeout(() => r(2), 4000)),
]);
// 2

PromiseRiceSuccess([
  Promise.reject(1),
  new Promise((resolve, reject) => setTimeout(() => reject(2), 4000)),
]);
// Uncaught (in promise) (2) [{…}, {…}]
JonDotsoy
  • 104
  • 1
  • 4