January 08, 2016

Javascript 'callbacks' - Use promises to make your life easier

When you use callbacks to resolve many functions and you need to wait for the result of one in order to move to the next function, this can often end up in a series of nested code which is difficult to read and soon you may end up in 'callback hell'. 

Myself and a few others have been working on a piece of work to load different HTML content into a single page as and when the user requests it without the page reloading, for a seamless experience. This got us thinking about our AJAX approach and if we can better it.

ES6 has many new, exciting and useful features including Promises which are much easier to read and use, an alternative to callbacks. We included ES6 in our development with Babel as a polyfill to transpile it back to ES5 for browsers that don't support it. We used the Fetch API, a replacement for XMLHttpRequest which uses promises. I won't go into the Fetch API but more reading can be found at Sitepoint.

  • Code is much more legible than callbacks
  • Cleaner code that is easier to maintain
  • Easier to debug
  • Asynchronous code can be written more easily
  • We can proceed with our code without knowing what value will be returned in the future

A promise provides resolve and reject functions to the provided callback. When the promise is created it is in a 'pending' state and it can then go on to one of two other states 'fulfilled' or 'rejected'. When it is fulfilled or rejected this value stays in place and it is immutable (it cannot be changed).

Creating a promise

var doThisPromiseFirst = function() {
   var promise = new Promise(function(resolve, reject){
      setTimeout(function() {
         console.log('doThisFirst completed');
         resolve({data: '123'});
      }, 2000);
   });
   return promise;
};
 
 
var doThisPromiseNext = function(someStuff) {
   var promise = new Promise(function(resolve, reject){
      setTimeout(function() {
         console.log('doThisNext completed');
         resolve({newData: someStuff.data + ' some more data'});
      }, 2000);
   });
   return promise;
};
 
var andThenPromiseThis = function(someStuff) {
   var promise = new Promise(function(resolve, reject){
      setTimeout(function() {
         console.log('andThenThis completed');
         resolve({result: someStuff.newData});
      }, 3000);
   });
   return promise;
};
 
doThisPromiseFirst() //A Promise object
   .then(doThisPromiseNext)  is well and fulfilled
   .then(andThenPromiseThis);

In the example we resolve and reject functions in the callbacks. I have chained promises (queued them up) to run aync actions one after the other. firstMethod returns a promise and each handler returns a promise as well. Each subsequent handler is called and resolved with the value of the previous function in the chain. Each then receives the result of the previous then's return value. If a promise has already resolved but then is called again, the callback immediately fires. If the promise is rejected and you call then after rejection, the callback is never called.

// Promise approach 
runFunction1(function(){..}) 
  .then(null, doFunction2) 
  .then(null, doFunction3) 
  .then(success, failure)


Error handling

Note that in the above basic example the second handler has a 'null' as its first argument call to the 'then. The first argument is called a fulfillment handler and if it is not a function then the returns promise must be resolved with the same value. This continues down the chain until a fulfillment handler i.e. 'success' is a function and handles the response. So, a 'then' can take two arguments, one for success and one for failure (fulfil and reject).)

Or instead of two arguments you could use a 'catch':

.catch(
    function(somethingWentWrong) { 
        console.log("Oops", somethingWentWrong); 
    })

The catch callback is executed when the promise is rejected.

Other uses:

  • Promise.all : you may want to fire many async functions and do something when they have all completed. Promise.all takes an array of promises and calls a callback when they have all resolved. However if there is a rejection the 'catch' will fire after the first rejection.
  • Promise.race : this fires as soon as any promise in an array has resolved or rejected.

I have scratched the surface here but Read more about promise here at HTML5Rocks and at MDN