Managing Asynchronous Code - Callbacks, Promises & Async/Await
Written by Mike James   
Monday, 21 March 2022
Article Index
Managing Asynchronous Code - Callbacks, Promises & Async/Await
The Callback And The Closure
onFulfilled and onRejected
Async and Await

If you want to ensure that asyncFunc1 finished and then asyncFunct2 was called  you need to use the fact that the then method returns a Promise object that resolves to the return value of the called handler.

That is then() returns a Promise for the value that its handlers, the code it eventually runs, return. 

This is a complicated idea but it means if the function that you use in then() returns a value generated by an asynchronous function then the Promise that then() returns waits for the async function to complete. 

For example:

var p1 = asyncFunc1();
p1.then(function (r) { console.log("first");
                       return asyncFunc2(); })
  .then(function (r) { console.log("second"); });

Now we have a situation in which the console log will always read "first" printed after asyncFunc1 completes and then "second" after asyncFunc2 completes. 

Using this chaining approach you can write a sequence of asynchronous actions which look a look like the code used in the synchronous case.

You can even start the chaining with the first function call and hide the Promise object completely:

asyncFunc1().then(function (r) {
   return asyncFunc2();
}).then(function (r) {
   return asyncFunc3();
}).then(function (r) {
   return asyncFunc4();
});

Which looks a lot like the synchronous 

r1=asyncFunc1();
r2=asyncFunc2();
r3=asyncFunc3();
r4=asyncFunc4();

As long as you have a good imagination.

onFulfilled and onRejected

Things are even better however because the then method has two possible handlers - one for the fulfilled state and one for the rejected:

then(onFulfilled,onRejected)

and there is a catch method which allows an onRejected handler to be added to a Promise to handle an error condition that has been cascaded down the chain. 

How this all works is subtle.

The key is that the then() method returns a Promise that resolves to either the return value of the handler that was run or to the state of the Promise one earlier in the chain if the handler was undefined. You can see that this could lead to propagation, mostly of unhandled error conditions down the chain.

This gives you a lot of scope in handling errors you can  choose to handle each error as it occurs:

asyncFunc1().then(function (r) {
               
    return asyncFunc2();
                  },
                  function(err){..return err;})
            .then(function (r) {
                   return asyncFunc3();  },
                  function(err){.. return err;})
            .then(function (r) {
                   return asyncFunc4();},
                  function(err){.. return err;})
);

Notice that when an error handler is called the next then recieves a new Promise resolves to the return value of the error handler. This results in the onFulfilled handler being called imediately and the chain continues as if there was no previous error.  

To bring the chain to a halt on an error you have to leave the onRejected handlers undefined until you are ready to handle the error. For example:

asyncFunc1().then(function (r) { 
                   return asyncFunc2();
                  })
            .then(function (r) {
                   return asyncFunc3();
                  })
            .then(function (r) {
                   return asyncFunc4();},
                  function(err){.. return err;}
);

If an error occurs in the call to asyncFunc1 then the Promise it returns is rejected and as there is no onRejected handler the Promise the then() generates is also rejected and so on till we reach the first onRejected handler. 

In most cases the final Promise generated has an onRejected handler added to it using the catch method. 

asyncFunc1().then(function (r) { 
                   return asyncFunc2();
                  })
            .then(function (r) {
                    return asyncFunc3();
                  })
            .then(function (r) {
                    return asyncFunc4();
                  })
            .catch(function(err){
                    console.log("ERROR");
                  });

 

You can see that this is vaguely similar the way an exception propagates until it reaches a handler and so we have a reasonable approximation to synchronous error handling. 

Once you move beyond these basics there are methods that allow you to combine Promise object in various ways that mimic simple synchronous flow of control. 

For example the all method returns a Promise when all of the specified promises have resolved. The race method returns a Promise when any one of the specified promises has resolved. 

There are other non-standard or almost standard methods that combine promises in this way.



Last Updated ( Wednesday, 23 March 2022 )