Managing Asynchronous Code - Callbacks, Promises & Async/Await
Written by Mike James   
Wednesday, 12 August 2015
Article Index
Managing Asynchronous Code - Callbacks, Promises & Async/Await
Promises
Async and Await

Promises

The callback is a continuation passing approach to asynchronous code - you pass where the program should continue from as a parameter, i.e. the callback. Promises or futures also have a long history in programming but rather than going into the computer science it is more realistic to focus on the sudden interest in promises caused by their adoption in JavaScript or ECMAScript 2015 to be precise. 

The biggest problem with trying to understand what promises give you is that most of the accounts start out by explaining the inner workings of a Promise object. This is very interesting and you can learn a lot about JavaScript by studying how Promise is implemented but it doesn't help much with how to make use of it. Even when accounts move on to using Promise they tend to spend a lot of time explaining how to convert asynchronous callback based functions into Promise based functions. Again this is interesting and essential if you you are going to make use of promises but it doesn't help you understand the key ideas that motivate wanting to use promises. 

So, no computer science, no implementation details and no adding promises to existing asynchronous code. Instead we are going to assume that we live in a world where all asynchronous code uses promises and it is just up to us to code using this fact.

Instead of accepting a call back the asynchronous function returns a Promise object. This can be in three possible states:

  • pending - the asynchronous operation is underway
  • fulfilled - the operation completed and its result is ready to use
  • rejected - the operation failed. 

Once a Promise object enters either the fulfilled or rejected state it doesn't change state again.

The eventual result of a promise is always a single value - which sounds restrictive until you know that it can be an object. 

For example:

var p1 = asyncFunc();

returns a Promise object imediately and gets on with whatever it is supposed to be doing to get the eventual result. 

It is often said that a Promise is a promise to return a result. 

So how do you get to process the result? 

The then method of the Promise object is the most common way to process the eventual result. 

For example:

p1.then(function(r){
 console.log(r);
});

When the Promise object enters the fulfilled state the functions registered with the then methods are executed with the result as their single parameter.

You can register multiple functions with a Promise object and they will be executed in the order that they were registered.

If a Promise object is in the fulfilled state when you register a function then it will be called almost immediately.

The reason why it is "almost" immediately is that all registered functions are called asynchronously and hence the function that is using the Promise has to end before any "then" functions are called.

All registered functions are called just once. 

For example:

var p1 = asyncFunc();
p1.then(function (r) { console.log("first"); });
p1.then(function (r) { console.log("second"); });
console.log("Finished");

WIll display

Finished
first
second

even if the asyncFunc is completed before the functions are registered. 

So far the Promise looks a lot like just a neater way of setting up a callback - and in many ways that really is all it is but there are some other advantages in doing things this way. 

The Promise object is returned immediately to the calling function and this can be used to set up complicated call sequences that will only happen long after the calling function has completed. 

For example you could set two async operations going using:

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

var p2=asyncFunc2();
p2.then(function (r) { console.log("second"); });

 

in this case which of the two would finish first isn't determined and the messages will be printed on the console in any order.

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 ( Friday, 31 March 2017 )