AngularJS Corner – Using promises and $q to handle asynchronous calls

Reference from: http://chariotsolutions.com/blog/post/angularjs-corner-using-promises-q-handle-asynchronous-calls/

The AngularJS $q service is said to be inspired by Chris Kowal’s Q library (github.com/kriskowal/q). The library’s goal is to allow users to monitor asynchronous progress by providing a “promise” as a return from a call. In AngularJS, the semantics of using a promise are:

A number of Angular services return promises: $http, $interval, $timeout, for example. All promise returns are single objects; you’re expected to research the service itself to find out what it returns. For example, for $http.get, the promise returns an object with four keys:data, status, headers, and config. Those are the same as the four parameters fed to thesuccess callback if you use those semantics:

We’re ignoring the error function and the promise’s error and update callbacks for this example. Beginners might wonder why we’re going to such lengths just to get an answer. But anybody who starts writing non-trivial services hits a wall in Angular – how do you pass a return back to a caller when the service returns before a callback is triggered?

Promises and Services

The semantics of Angular dictate that you use promises as a sort of ‘callback handle’ – do something asynchronous in a service, return a promise, and when the asynchronous work is done, the promise’s then function is triggered. With that in mind, let’s build a simple controller and service sample for getting data, and putting it on the page:

Now you’re able to call the $http method asynchronously, get a result and update your user interface. The service doesn’t have to understand the UI semantics – you’re not passing$scope into your getMovie function, so all is good. Except…

What about post processing

The downside of blindly passing your $http.get promise back to the controller is that the controller has to deal with the result itself. What if you want the service to post-process the result instead? More importantly, what if you want to deal with the $http errors in the service layer, so that the controller doesn’t need to handle redirects, 404s, etc?

In more sophisticated cases you can solve this by building your own promise. Let’s refactor the service to use a promise internally, so we can handle the result in the service and bring back the payload we want.


 

Hopefully, the reason for building a nested promise structure is obvious – you can control both the input and output of the call, log errors appropriately, transform the output, and even provide status updates with deferred.notify(msg).

But there is a better, more foolproof way…

Composing Promises

You can avoid creating your own deferred object and managing a separate promise by transforming your response within a then method and returning a transformed result to the caller automatically. For example, let’s say you want to wrap the data of an $http response without having to deal with your own deferred object. Here is a service method that will do so:

Now the content returned in the then of the service method will be a chained promise that transforms the output. The controller then can do the same non-http work that it did in the above example:

So, we’ve reduced the amount of code we have to write to achieve the same result, and we don’t have to worry about what happens when the Ajax call fails – it will just fail before calling the chained then function.

Handling problems in nested service calls

Now that we’re using an inner return in the success function, the promise code will automatically sense the lack of an error function and just abort the call with the default $http error object. Nice, eh? But you may wonder how you can transform the error? Simple, just throw it!

…and now the error returned will be a single string, not the $http error with data, status, headers and config properties.

Doing more than one thing at a time

This is a powerful service. Even more interesting is a case when you need to process a number of asynchronous activities simultaneously. The $q.all function lets you trigger several callbacks at the same time, and use a single then function to join them all together.

I put together a sample using $q.all to show you how the async semantics work when dealing with a few in-flight promises. Here are the highlights.

In this sample I’m allowing the user to fetch several URLs at the same time using $http.get. The user enters each URL in a text input box, and when ready clicks a button to trigger the calls. A single service processes them, awaits all answers, and returns a message.

In this example, the payload is an array of objects containing an ‘url’ property:

For each URL, a promise is created by executing $http.get, and the promise is added to an array. The $q.all function accepts an array of promises, and converts all results into a single promise which contains an object with each answer. You can envision the result like this:

If those three calls were made, the code above converts the results to JSON and returns them to the caller.

What about failures?

The difficulty in dealing with multiple in-flight promises this way is in dealing with a failure case. If any of the promises we’re waiting on fails, the entire batch fails. Try playing with the plunker link above and entering an off-site url like http://google.com. You’ll see that the entire call becomes a failure.

You still need infrastructure and architecture

Getting on my soapbox here, for anything business-related multiple in-flight promises are a bad idea. Providing a server infrastructure with transactional semantics, orchestration, and a coarse-grained single call for the client seems to be the rational way to go. I can see multiple in-flight promises for aggregating content such as images for a mosaic, but for anything data related it’s a no-brainer to avoid. And imagine the concept of a transaction in this world, then forget about it because you’re running in a browser.

Wrap-up

In short, promises fuel AngularJS asynchronous operations. Anything needing to run in the background will need to coordinate with a caller such as a controller or directive, and the promise API is the way to go.

518 total views, 1 views today

Trả lời