Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: James Allen and Matt Gifford
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: James Allen@CFJamesAllen ) and Matt Gifford@coldfumonkeh )

Learning About Promises By Implementing The Race() Method Algorithm

By Ben Nadel on

If you look at the Q documentation, it doesn't look like it implements the .race() method. But, if you look at the source-code, the .race() method is indeed there. And, when I saw how Q implemented it, I was reminded of just how interesting Promises are. When I had thought that .race() was missing, I tried to implement it myself; and, upon seeing the Q implementation, I realized how sparse my understanding of Promises are, despite the fact that I use them all the time.


 
 
 

 
 
 
 
 

The .race() method takes a collection of promises (and / or other values) and returns a promise. The returned promise is resolved or rejected using the result of the first promise in the collection that becomes settled. Essentially, all promises in the collection are racing to be the first one to return with a value (either in resolution or rejection).

When I went to implement the race() method, I fixated on this "first to return" concept and ended-up building in guards to ensure that my internal deferred value wasn't resolved or rejected more than once:

  • // Require the core node modules.
  • var chalk = require( "chalk" );
  • var Q = require( "q" );
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  •  
  • // Pit five randomly-timed promises against each other.
  • var promise = race([
  • randomPromise(),
  • randomPromise(),
  • randomPromise(),
  • randomPromise(),
  • randomPromise()
  • ]);
  •  
  • // Log the winner of the race.
  • promise.then(
  • function handleResolve( value ) {
  •  
  • console.log( chalk.magenta( "Winner:" ), value );
  •  
  • }
  • );
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  •  
  • // I return a promise that is resolved or rejected by the first promise in the given
  • // collection that becomes settled.
  • // --
  • // CAUTION: These were the implementation details that I had in my head when I first
  • // thought about how to implement the race() algorithm.
  • function race( promises ) {
  •  
  • var deferred = Q.defer();
  • var isRaceOver = false;
  •  
  • for ( var i = 0 ; i < promises.length ; i++ ) {
  •  
  • promises[ i ].then(
  • function handleResolve( value ) {
  •  
  • // NOTE: Make sure we only resolve the root promise once.
  • if ( ! isRaceOver ) {
  •  
  • isRaceOver = true;
  • deferred.resolve( value );
  •  
  • }
  •  
  • },
  • function handleReject( reason ) {
  •  
  • // NOTE: Make sure we only resolve the root promise once.
  • if ( ! isRaceOver ) {
  •  
  • isRaceOver = true;
  • deferred.reject( reason );
  •  
  • }
  •  
  • }
  • );
  •  
  • }
  •  
  • return( deferred.promise );
  •  
  • }
  •  
  •  
  • // I create a promise that resolves in a randomly selected timeout.
  • function randomPromise() {
  •  
  • var deferred = Q.defer();
  • var value = Math.floor( Math.random() * 500 );
  •  
  • console.log( chalk.grey( "Creating promise for [", value, "] milliseconds." ) );
  •  
  • setTimeout(
  • function resolveTimer() {
  •  
  • deferred.resolve( value );
  •  
  • },
  • value
  • );
  •  
  • return( deferred.promise );
  •  
  • }

As you can see, I'm iterating over the collection of promises, binding to them, and then keeping track of whether or not a winner has already been selected. If a promise already returned, all subsequent resolutions or rejections get ignore. And, when we run the above code, we get the following terminal output:


 
 
 

 
 Learning about promises by implementing the race() method. 
 
 
 

As you can see, the first settled promise provided the resolution for the root promise.

This works; but, when I saw the internal Q implementation, I realized that my approach didn't leverage the nature of Promises. And, in failing to think like a Promise, I was doing much more work than I actually had to.

Here's the same demo. But, this time, I'm using an implementation that is much closer to what Q is doing internally.

  • // Require the core node modules.
  • var chalk = require( "chalk" );
  • var Q = require( "q" );
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  •  
  • // Pit five randomly-timed promises against each other.
  • var promise = race([
  • randomPromise(),
  • randomPromise(),
  • randomPromise(),
  • randomPromise(),
  • randomPromise()
  • ]);
  •  
  • // Log the winner of the race.
  • promise.then(
  • function handleResolve( value ) {
  •  
  • console.log( chalk.magenta( "Winner:" ), value );
  •  
  • }
  • );
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  •  
  • // I return a promise that is resolved or rejected by the first promise in the given
  • // collection that becomes settled.
  • function race( promises ) {
  •  
  • var deferred = Q.defer();
  •  
  • for ( var i = 0 ; i < promises.length ; i++ ) {
  •  
  • // We can pass the same resolve / reject methods to all of the given promises
  • // because the root promise can only be "settled" once. As such, the first
  • // promise to return is one that resolves the root promise. All subsequent
  • // requests to settle the root promise will be implicitly ignore.
  • promises[ i ].then( deferred.resolve, deferred.reject );
  •  
  • }
  •  
  • return( deferred.promise );
  •  
  • }
  •  
  •  
  • // I create a promise that resolves in a randomly selected timeout.
  • function randomPromise() {
  •  
  • var deferred = Q.defer();
  • var value = Math.floor( Math.random() * 500 );
  •  
  • console.log( chalk.grey( "Creating promise for [", value, "] milliseconds." ) );
  •  
  • setTimeout(
  • function resolveTimer() {
  •  
  • deferred.resolve( value );
  •  
  • },
  • value
  • );
  •  
  • return( deferred.promise );
  •  
  • }

Here, you can see that the logic is significantly reduced. In this approach, I'm not worrying about whether or not a winner has already been selected because Promises already do that for us. A promise, in general, cannot be settled more than once - any attempt to resolve or reject a settled promise will be ignored. As such, we don't have to reimplement that logic in our race() function. Once the winning promise causes our root promise to be settled, all the subsequent promise resolutions will be ignored implicitly.

Another Promise feature that we leverage in this approach is the fact that resolve() and reject() methods can be passed around as naked functions. This is due to the fact that they make no use of the "this" binding internally; all references within these methods are pulled from the lexical bindings. As such, we can hook each promise directly into our deferred value by using the deferred's resolve() and reject() methods as our parameters to the .then() method.

And, when we run the above code, we get a similar output:


 
 
 

 
 Learning about promises by implementing the race() method. 
 
 
 

Promises are awesome; but, they are complex and multi-faceted. If you don't spend time thinking about how promises work, you can easily end up writing code that is far more complicated than it needs to be. Taking a look at how various promise libraries implement their utility functions (like .race()) can shed a lot of light on the Promise mindset. It's definitely worth taking some time to dig around.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.