JavaScript Async - Composing Promises
Written by Ian Elliot   
Monday, 13 July 2020
Article Index
JavaScript Async - Composing Promises
Any

Although async and await are the stars of the async programming world, Promises still have advantages - in particular you can write code which waits for some combination of Promises to resolve.  Here we look at how to write your own promise-combining functions.

This is an extract from the recently published JavaScript Async: Events Callbacks, Promises & Async/Await.

Now Available as a Book:

 JavaScript Async

cover

You can buy it from: Amazon

Contents

  1. Modern JavaScript (Book Only)
  2. Events,Standard & Custom
  3. The Callback
      extract - The Callback & The Controller
  4. Custom Async - setTimeout, sendMessage & yield
      extract - Custom Async
      extract - Avoiding State With Yield 
  5. Worker Threads
      extract - Basic Worker ***NEW
      extract - Advanced Worker Threads 
  6. Consuming Promises 
  7. Producing Promises
      extract - The Revealing Constructor Pattern
     
    extract - Returning Promises
     
    extract - Composing Promises
  8. The Dispatch Queue
      extract - Microtasks
  9. Async & Await
      extract -  Basic Async & Await
      extract -  DoEvents & Microtasks
  10. Fetch, Cache & Service Worker
      extract - Fetch  
      extract - Cache
     
    extract -  Service Workers

Composing Promises

One of the more advanced aspects of using Promises is writing functions that work with multiple Promises in various ways. Many Promise libraries provide utilities such as any, which is fulfilled if any Promise is; some, fulfilled if a fixed number of Promises are; and so on. The JavaScript Promise standard provides just two which we met in the previous chapter.

  • all fulfilled if all are and returns an array of values. It rejects if any Promise rejects.

  • race fulfilled if one fulfills and rejects if one rejects and returns a single value or reason for rejecting.

There is also a variation on all:

  • allSettled resolves when all Promises have fulfilled or rejected and returns an array of values and reasons for rejection.

Use all if the Promises are dependent and any one failing means they all fail. Use allSettled if you can still continue if some of the Promises have failed.

There are also a new Promise combining functions which should be in ECMAScript 2020:

  • any fulfilled by the first Promise that fulfills and returns its value. If no Promise fulfills then it rejects.

Rather than providing lots of different standard Promise-composing functions, it is simpler to learn how to write your own that do exactly what you want. Usually the problem is how to handle rejects, and this you can tailor to the situation.

Our Own Race Function

As an example, let's implement our own simple version of the race function, race1, which also returns a Promise that resolves when the first of two Promises resolve. It is always a good idea to try to implement an idea as simply as possible and then extend it to more realistic examples.

It turns out that race1 is very easy to write:

function race1(p1, p2) {
  var p = new Promise(
        function (resolve, reject) {
                p1.then(
                       function (value) {
                            resolve(value);
                       },
                       function (error) {
                            reject(error);
                       }
          );
                p2.then(
                       function (value) {
                           resolve(value);
                        },
                       function (error) {
                           reject(error);
                        }
               );
        });
        return p;
}

All that happens is that we return a new Promise object that is resolved or rejected when any of the Promises provided as arguments resolves or rejects. To make this happen we use the then method of the two Promises to resolve or reject our new Promise.

Notice that we don't do anything to stop the other Promises from completing. It is generally difficult to cancel a pending asynchronous operation. Also notice that as a Promise is immutable, we don't need to worry about later Promises settling and trying to set the returned Promise to something different.

This can be used to get the first Promise to resolve or reject:

myPromise1 = delay(1000, 0);
    myPromise2 = delay(2000, 0);
    myTotalPromise = race1(myPromise1, myPromise2);
    myTotalPromise.then(
           function (value) {
                console.log("success");
                console.log(value);
           },
           function (value) {
                console.log("fail");
                console.log(value);
           });

In this case you will see a delay of 2000ms. Of course, you could add the race1 function as a method to the Promise object.

Any Number Of Promises

Extending the race function to work with any number of Promises can be done using JavaScript's iterable i.e an object that behaves like an Array.

The key is to use the map member which is supported by most browsers from IE 8 on:

function race1(args) {
     var p = new Promise(
               function (resolve, reject) {
                  args.map(
                  function (p) {
                                p.then(
                                  function (value) {
                                       resolve(value);
                                   },
                                   function (error) {
                                       reject(error);
                                   }
                                );
                   });
             });
      return p;
 }

You can see that all that happens is that the then method is used for each of the elements in the array. If you don’t want to use map you can use a for loop instead. To make this work you have to remember to pass an array of Promises:

myPromise1 = delay(2000, 0); 
myPromise2 = delay(1000, 0); 
myTotalPromise = race1([myPromise1, myPromise2]);

This is an implementation of the standard race function, but it is generally held that it isn't very useful as it will return the first function to complete, even if it rejects.



Last Updated ( Saturday, 12 September 2020 )