Sunday, March 20, 2016

Promises in AngularJS. Part II. $q service.

I have already blogged about Promises in AngularJS 1.x. This is the second part which describes the Angular's $q service. The $q service can be used in two different ways. The first way mimics the Q library for creating and composing asynchronous promises in JavaScript. The second way mimics the ECMAScript 2015 (ES6) style. Let's begin with the first way. First of all, you have to create a deferred object by $q.defer().
 
var deferred = $q.defer();
 
A deferred object can be created within an asynchronous function. The function should return a promise object created from the deferred object as follows:
 
return deferred.promise;
 
A promise is always in either one of three states:
  1. Pending: the result hasn't been computed yet
  2. Fulfilled: the result was computed successfully
  3. Rejected: a failure occurred during computation
When the asynchronous function finished the execution, it can invoke one of the two methods:
deferred.resolve(...)
deferred.reject(...)
The first call deferred.resolve(...) puts the promise into the fulfilled state. As result a success callback will be invoked. The second call deferred.reject(...) puts the promise into the rejected state. As result an error callback will be invoked. It is also possible to invoke
 
deferred.notify(...)
 
during the function's execution to propogate some progress from the asynchronous function to an update callback. All three callbacks can be registered on the promise as parameters of the function then:
var promise = someAsynchronousFunction();
promise.then(function(value) {
    // success
    ...
}, function(reason) {
    // failure
    ...
}, function(update) {
    // update
    ...
});
Let's implement an example. We will take setTimeout() as an asynchronous function. In the real application, you will probably use some other asynchronous services. In the setTimeout(), we will generate a random number after 1 sek. If the number is less than 0.5, we will invoke deferred.resolve(random), otherwise deferred.reject(random). The entire logic is implemented in the controller PromiseController.
var app = angular.module('app', []);
app.controller('PromiseController', PromiseController);

function PromiseController($q) {
    var _self = this;
    this.message = null;

    var asyncFunction = function() {
        var deferred = $q.defer();

        setTimeout(function() {
            var random = Math.random().toFixed(2);

            if (random < 0.5) {
                deferred.resolve(random);
            } else {
                deferred.reject(random);
            }
        }, 1000);

        return deferred.promise;
    }

    this.invokeAsyncFunction = function() {
        var promise = asyncFunction();

        promise.then(function(message) {
            _self.message = "Success: " + message;
        }, function(message) {
            _self.message = "Error: " + message;
        });
    }
}
As you can see, the asynchronous function asyncFunction is invoked in the controller's method invokeAsyncFunction. The function asyncFunction returns a promise. In the success case, the promise gets fulfilled and the first registered success callback gets executed. In the error case, the promise gets rejected and the second registered error callback gets executed. The invokeAsyncFunction is bound to the onclick event on a button.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta content="IE=edge" http-equiv="X-UA-Compatible" />
    <script src="https://code.angularjs.org/1.4.8/angular.js"></script>
</head>

<body ng-app="app" ng-controller="PromiseController as ctrlPromise">
    <div ng-bind="ctrlPromise.message"></div>
    <p></p>
    <button ng-click="ctrlPromise.invokeAsyncFunction()">
        Invoke asynchronous function
    </button>

    <script src="controller.js"></script>
</body>
</html>
The message in GUI looks like as follows (success case as example):


The Plunker of this example is available here. Here is another picture that visualize the relation between methods of the deferred object:


As you can see, the asynchronous function doesn't return the deferred object directly. The reason for that is obvious. If the deferred object would be returned instead of deferred.promise, the caller of the asynchronous function could be able to trigger callbacks by invoking deferred.resolve(...), deferred.reject(...) or deferred.notify(...). For this reason these methods are protected from being invoking from outside by the caller.

The example above can be rewritten in the ECMAScript 2015 (ES6) style (I mentioned this way at the beginning). A promise in ECMAScript 2015 can be created as an instance of Promise object.
var promise = new Promise(function(resolve, reject) {
    ...
    if(...) {
        resolve(value); // success
    } else {
        reject(reason); // failure
    }
});
Our asyncFunction function looks in this case as follows:
var asyncFunction = function() {
    return $q(function(resolve, reject) {

        setTimeout(function() {
            var random = Math.random().toFixed(2);

            if (random < 0.5) {
                resolve(random);
            } else {
                reject(random);
            }
        }, 1000);
    });
}
The remaining code stays unchanged. Let's go on. The next question is, how can we produce a rejection in success or error callbacks? For instance, you check some condition in a callback and want to produce an error if the condition is not fulfilled. Sometimes, we also want to forward rejection in a chain of promises. That means, you catch an error via an error callback and you want to forward the error to the promise derived from the current promise. There are two ways to achieve this qoal. The first one consists in using the $q.reject(...) like shown below.
var promise = someAsynchronousFunction();
promise.then(function(value) {
    // success
    ...
    if(someCondition) {
        $q.reject("An error occurred!");
    }
}, function(reason) {
    // failure
    ...
}).catch(function(error) {
    // do something in error case
    ...
});
In our example, we will adjust the function invokeAsyncFunction in order to check very small values (smaller than 0.1).
this.invokeAsyncFunction = function() {
    var promise = asyncFunction();

    promise.then(function(random) {
        if (random < 0.1) {
            return $q.reject("Very small random value!");
        }

        _self.message = "Success: " + random;
    }, function(random) {
        _self.message = "Error: " + random;
    }).catch(function(error) {
        _self.message = "Special error: " + error;
    });
}


A Plunker example is available here. Keep in mind the difference between deferred.reject(...) and $q.reject(...). The call deferred.reject(...) puts the corresponding promise into the rejected state. The call $q.reject(...) creates a new promise which is already in the rejected state.

The second way to produce a rejection in success or error callbacks consists in throwing an exception with throw new Error(...).
this.invokeAsyncFunction = function() {
    var promise = asyncFunction();

    promise.then(function(random) {
        if (random < 0.1) {
            throw new Error("Very small random value!");
        }

        _self.message = "Success: " + random;
    }, function(random) {
        _self.message = "Error: " + random;
    }).catch(function(error) {
        _self.message = "Special error: " + error;
    });
}
AngularJS will catch the exception, create a promise in the rejected state and forward it to the next block in the chain of promises. In the example, the error will be forwarded to the catch block. One downside of this approach with throw new Error(...) is that the error will be logged in the console. Picture from the Chrome Dev Tools:


But well, it works as designed and probably it is even an advantage to see thrown errors in the console.

There is also an opposite method $q.when(...) which returns an immediately resolved promise. The documentation says: $q.when(...) "wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. This is useful when you are dealing with an object that might or might not be a promise, or if the promise comes from a source that can't be trusted." You can e.g. wrap an jQuery Deferred Object with $q.when(...) or simple write
$q.when("Finished!").then(
    function handleResolve(value) {
        console.log("Resolved with value: ", value);
    }
);
and see Resolved with value: Finished! in the console. The alias of $q.when(value) is $q.resolve(value). This was introduced later in order to maintain naming consistency with ECMAScript 2015.

Last but not least is the method $q.all(promises) where promises is an array of multiple promises. This call returns a single promise that is resolved when all promises in the given array gets resolved.
var promise1 = someAsynchronousFunction1();
var promise2 = someAsynchronousFunction2();

$q.all([promise1, promise2]).then(function(result) {
    console.log("Promises " + result[0] + " and " + result[1] + " finished their work successfully");
});
As you can see, the result passed into the callback function is an array of two outcomes - the outcome of the first and the outcome of the second callback.

3 comments:

Note: Only a member of this blog may post a comment.