Skip to content

Instantly share code, notes, and snippets.

@unscriptable
Forked from briancavalier/ambiguous-race.js
Last active September 25, 2019 13:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save unscriptable/06f8984d6cb0fd6be1c0 to your computer and use it in GitHub Desktop.
Save unscriptable/06f8984d6cb0fd6be1c0 to your computer and use it in GitHub Desktop.
Promise.race is a lie
// This is the function we will use to call Promise.race().
// logWinner is simply a function that races two promises
// and logs the "winner" of the race as a side effect.
function logWinner (p1, p2) {
Promise.race([p1, p2]).then(console.log.bind(console));
}
// Here are 2 promises, p1 and p2. p2 always resolves
// first, since p1 resolves in 20 ms, and p2 resolves
// in 10 ms. By any reasonable definition of "race",
// p2 should be the winner.
var p1 = new Promise(function(resolve) {
setTimeout(function() { resolve('p1'); }, 20);
});
var p2 = new Promise(function(resolve) {
setTimeout(function() { resolve('p2'); }, 10);
});
// Given the same inputs, you would expect that Promise.race
// would behave the same *no matter when you call it*. However,
// Promise.race behaves differently depending on *when* it is
// called. In an async environment (e.g. browsers, node),
// you can never determine when something will be called in
// all but the most trivial cases.
// If Promise.race is called *before* any of the raced promises
// are resolved, logWinner logs the correct winner, "p2".
setTimeout(function() { logWinner(p1, p2); }, 0);
// If Promise.race is called *after* any of the raced promises
// are resolved, logWinner logs the lowest resolved promise
// in the array, "p1".
setTimeout(function() { logWinner(p1, p2); }, 50);
// Therefore, Promise.race is non-deterministic.
@majew7
Copy link

majew7 commented Oct 4, 2017

This is a nice example... however my conclusion is different. Since you changed the context (delayed calling race() by 50ms), then the behavior is expectedly different.

My conclusion, is that Promise.race() is working as designed.

@roscioli
Copy link

I know this is an old post, and perhaps your opinion has changed. But I would like to say (in the hopes that future readers can not be misled or maybe for someone else to have me question my thought process) thatPromise.race is certainly not non-deterministic.

For a function to be deterministic, you should always receive the same output if you give the function the exact same input. In this case, the input to Promise.race differs between setTimeout(function() { logWinner(p1, p2); }, 0) and setTimeout(function() { logWinner(p1, p2); }, 50) calls (because p1 is in a different, resolved state when it is called a second time, as well as p2). So, like @majew7 said, the output is expectedly different as well since the inputs are now different.

Node has no state for when a promise was resolved, so Promise.race certainly can not make the call on which promise resolved first. Thus, Promise.race is behaving very functionally and deterministically.

@jadengeller-asana
Copy link

jadengeller-asana commented Apr 26, 2018

The race doesn't begin until you call Promise.race. If both promises start at the finish line, then it (deterministically!) chooses the first winner in order of arguments.

@fedeghe
Copy link

fedeghe commented Oct 8, 2018

I do not see the point for two main reasons:

  • your are using the same promises in the second logWinner call (passing already solved pros)
  • the executor of the promise is started at the promise definition .

then should be clear that since at the first call p2 is the winner, but on the second call also p1 is solved and since in the race is checked before p2, p1 wins on the second run,... and everything makes sense.
As a small counter-proof is enough to set higher timeouts in the promises definition (enough higher than the timeouts for the logWinner calls):

var base = 50,
   to1 = 52,
   to2 = 55;
// then p1, p1
// but try inverting them you will get p2, p2

function logWinner(p1, p2) {
    Promise.race([p1, p2]).then(console.log.bind(console));
}
var p1 = new Promise(function (resolve) {
    setTimeout(function () { resolve('p1'); }, to1);// more than `base`
});
var p2 = new Promise(function (resolve) {
    setTimeout(function () { resolve('p2'); }, to2); // more than `base`
});
setTimeout(function () { logWinner(p1, p2); }, 0);
setTimeout(function () { logWinner(p1, p2); }, base);

I hope this could clarify

Have fun

@joeyhub
Copy link

joeyhub commented Sep 25, 2019

Please see these:

You case looks like a new variation of the first I've not seen before though it's not entirely unexpected.
By the time you call Promise.race, both promises are resolved. At this point Promise.race doesn't have the information to order them.

The work around that would be to check if resolved immediately after hooking it to the event in the correct order up front though I don't know of an explicit mechanism for that (it's an encapsulate state).

Basically do Promise.race() for each added timer? It's annoying as it needs to be order aware that things should finish in that way but not really much you can do about it. Technically speaking if the issue I found is fixed then it could be possible to put an autoincrement order id on the promises. Promise.race only works properly when all or all but one promises are in a pending state.

It's turning up to the finishing line and seeing both are there already. It doesn't have an action replay.

Thanks for the assist, I missed that in my POC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment