October 29, 2015

Taming asynchronous JavaScript with Promises

If you've ever written some non-trivial code in JavaScript, then you have already embraced the AJAX world. You also know that AJAX is the future of Web app development.


Javascript asynchronous

Thus, if you work with AJAX libraries, then you've already queried exposed API endpoints in an asynchronous way. Asynchronous means that function calls and operations don’t block the main thread while they execute.

Let's have a look at one example, where we want to query an API with the adverts of a user, and then render them. This could look like the following example:

var adverts = [];
function renderAdvertsOnPage(adverts) {
    $.each(adverts, function (index, advert) {
        $('ul.adverts-list').append('<li>' + advert.description + '</li>')
    });
}
$.get(‘/adverts’, function(data) {
  adverts = data;
});
renderAdvertsOnPage(adverts);

What happens is that $.get is called but it does not block the main thread. Then, the next operation (rendering) is performed even if the first call hasn’t returned anything. Thus adverts will be an empty array, and we will render nothing.


The callback pattern

In computer programming, a callback is a piece of executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at some convenient time.1
In JavaScript, functions are first-class objects, i.e. they are objects and can be manipulated and passed around just like any other object. They are Function objects.

JavaScript allows us to define a function which expects another function as a parameter, known as a callback or callback function. The callback function can be called once or several times, sychronous or asynchronous, at an appropriate time.


Let's rewrite the above example:

var adverts = [];
function renderAdvertsOnPageCallback(adverts) {
    renderAdvertsOnPage(adverts);
}
$.get(‘/adverts’, function renderAdvertsOnPageCallback(adverts) {
    renderAdvertsOnPage(adverts);
});
// or simply pass the renderAdvertsOnPage function reference
$.get(‘/adverts’, renderAdvertsOnPage);

What happens is that when $.get call returns, the callback function will be called. The response which contains an array of adverts is then passed on to the render function.

The callback hell

Often we need to do successive asynchronous calls. This is happening when every successive call uses the previous call's result to perform its own call.

As an example, let's assume we're trying to perform the following steps:
  1. Login
  2. Get user adverts
  3. Get adverts payment details
  4. Render information to the user
Using function callbacks, these successive calls could look like this:

login(function (user) {
    loadUserAdverts(user, function(adverts) {
        loadPaymentDetails(adverts, function(paymentDetails) {
            displayInformation(paymentDetails);
        });
    });
});
Using the above described approach, we are facing some problems that arise when code marches to the right faster than it marches forward 2. The problem is not the way how callback functions work, it's just a consequence of using them. There are some best practices for writing better asynchronous code. For example this article provides tips on improving readability, e.g. by naming callback functions (and thus avoiding anonymous functions) or by extracting functions outside the nested block. Following this, you do indeed achieve better readability, but this does not address the real problem.

The solution: Promises

A promise represents the eventual result of an asynchronous operation.3
Syntax:
var promise = new Promise(function(resolve, reject) { ... });
Where the first argument resolves the promise, the second one rejects it.
A promise is in the one of these states 4:
  • pending: initial state, not fulfilled or rejected.
  • fulfilled: meaning that the operation completed successfully.
  • rejected: meaning that the operation failed.
The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled. 5 Therefore, we can interact with the promise in the following way:
promise.then(onFulfilled, onRejected);
A promise may be called multiple times on the same promise6. Therefore, we can rewrite again the above promise in the following way:
promise
    .then(onFulfilled);
    .then(null, onRejected);
The only difference is that then returns a new promise, so if an exception is thrown in any of the first promises handler, the exception will be propagated / handled in the second promise handler - if defined.
Many libraries which are implementing the promises specifications provide two shorthand methods which are actually doing the same thing.
Few examples: 
  • jQuery: 
        $.getJSON('someUrl')
            .done(function(data) {
            })
            .fail(function(err) {
            })
  • Q
        getQPromise('someUrl')
            .then(function(data) {
            })
            .fail(function(err) {
            })
With these insights on promises in mind, we can now flatten the pyramid, and rewrite the previous example code.

By using chaining utility 7 of promises, therefore we can rewrite the previous example. Promises can be chained inside or outside handlers.
Chaining inside handler is basically used when you need to capture multiple input values in your closure:
loginWith(loginData) // return initial promise
    .then(function (userDetails) {
        return loadUserAdverts(userDetails)
            .then(function (userDetails, adverts) {
                return loadPaymentDetails(adverts); // return another promise
        })
    })
    .then(function (advertsSummaryList) {
        // render advertsSummaryList
    });
Or we can chain the promise outside handler:
loginWith(loginData) // return initial promise
    .then(function (userDetails) {
        return loadUserAdverts(userDetails); // return another promise
    })
    .then(function (adverts) {
        return loadPaymentDetails(adverts); // return another promise
    })
    .then(function (advertsSummaryList) {
        // render advertsSummaryList
    });
This can be simplified by passing on only the function references:
loginWith(login)
    .then(loadUserAdverts)
    .then(loadPaymentDetails)
    .then(function (advertsSummaryList) {        
    // render advertsSummaryList
    })
So far, we've seen the chaining benefit of using promises. Other great benefits of using promises are (which will not be covered in this article):
  • Propagation
  • Combination
  • Sequences
Nonetheless, the promise's then method purpose is still not properly understood. However useful, the propagation/combination/sequences utilities are not the main advantage of using promises. The real problem with asynchronous functions is that they cannot return a value or throw an exception, because nobody is there to handle the value or catch the exception. They are just not ready in time.

So what promises offer is that they emulate the sychronous functions behavior.
  • Synchronous functions:
    • return values 
    • throw exceptions
  • Promises:
    • become fulfilled by a value
    • become rejected with an exception

Therefore the following asynchronous code:
getPromiseOf(login)
    .then(function (user) {
        var userId = user.id;
        return userId;
    })
    .then(loadUserAdverts)
    .then(loadPaymentDetails)
    .then(
    function displayInformation(data) {
        // Render data
    }, function handlError(error) {
        // Show error
    })
parallels the following synchronous code:
try {
    // blocking
    var user = login(userData); 
    // blocking x 2
    var paymentData = loadPaymentDetails(loadUserAdverts(user.id));  
    // render paymentData
} catch (error) {
    // show error
}

As a conclusion, then is not a mechanism for attaching callbacks to an aggregate collection. It’s a mechanism for applying a transformation to a promise, and yielding a new promise from that transformation. Without these transformations being applied, you lose all the power of the synchronous/asynchronous parallel, and your so-called “promises” become simple callback aggregators.8

We're going to explore these four 8 possibilities (we're going to use Q library utilities for this purpose) :
1. Fulfilled, fulfillment handler returns a value: simple functional transformation:
Q
  .fcall(function(){
    return 'OK';
  })
  .then(function(response){
    console.log('Step 1. Success: '+ response);
    return response;
  })
  .then(function(response){
    console.log('Step 2. Success: ' + response);
    return response;
  })
  .then(null, function(response){
    console.log('Step 3. Failed: ' + response);
  });
Output:
"Step 1. Success: OK"
"Step 2. Success: OK"

2. Fulfilled, fulfillment handler throws an exception: getting data, and throwing an exception in response to it:
Q
  .fcall(function(){
    return 'OK';
  })
  .then(function(response){
    console.log('Step 1. Success: '+ response);
    throw new Error(response);
  })
  .then(function(response){
    console.log('Step 2. Success: ' + response);
    return response;
  })
  .then(null, function(response){
    console.log('Step 3. Failed: ' + response);
  });
Output:
"Step 1. Success: OK"
"Step 3. Failed: Error: OK"

3. Rejected, rejection handler returns a value: a catch clause got the error and handled it:
Q
  .fcall(function(){
    throw new Error('NOT OK');
  })
  .then(null, function(response){
    console.log('Step 1. Failed but handled: ' + response);
    return 'OK';
  })
  .then(function(response){
    console.log('Step 2. Success: '+ response);
    return response;
  });
Output:
"Step 1. Failed but handled: Error: NOT OK"
"Step 2. Success: OK"

4. Rejected, rejection handler throws an exception: a catch clause got the error and re-threw it (or a new one):
Q
  .fcall(function(){
    throw new Error('NOT OK');
  })
  .then(null, function(response){
    console.log('Step 1. Failed but handled (rethrow exception): ' + response);
    throw new Error('NEWER NOT OK');
  })
  .then(function(response){
    console.log('Step 2. Success: '+ response);
    return response;
  })
  .then(null, function(response){
    console.log('Step 3. Failed but handled: ' + response);
    return response;
  })
  .then(function(response){
    console.log('Step 5. Success: '+ response);
    return response;
  });
Output:
"Step 1. Failed but handled (rethrow exception): Error: NOT OK"
"Step 3. Failed but handled: Error: NEWER NOT OK"
"Step 5. Success: Error: NEWER NOT OK"

Conclusion

This was just a brief introduction to Javascript's new Promises API. It's fair to say that the concept of the promises is not easy to understand. However, using them gives us a lot of advantages and flexibility in writing asynchronous code.

Note: The promises is a new technology, part of the ECMAScript 2015 (ES6) Standard. Its specifications are described in more details on the official page. As it is a new technology (https://developer.mozilla.org), the official implementation is not supported by all browsers. Thus, there are many unofficial implementations, a list of which can be found here.


References:

Sources:

No comments:

Post a Comment