Software Development
The AngularJS Promise DSL Freek Wielstra 11 Aug, 2014
$.get('api/gizmo/42', function(gizmo) { console.log(gizmo); // or whatever });This is pretty neat, but, it has some drawbacks; for one, combining or chaining multiple asynchronous processes is tricky; it either leads to a lot of boilerplate code, or what's known as callback hell (nesting callbacks and calls in each other):
$.get('api/gizmo/42', function(gizmo) { $.get('api/foobars/' + gizmo, function(foobar) { $.get('api/barbaz/' + foobar, function(bazbar) { doSomethingWith(gizmo, foobar, bazbar); }, errorCallback); }, errorCallback); }, errorCallback);You get the idea. In Javascript however, there is an alternative to dealing with asynchronous code: Futures, although in Javascript they're often referred to as Promises. The CommonJS standards committee has released a spec that defines this API called Promises. The concept behind promises is pretty simple, and has two components:
var deferred = $q.defer();Next, we'll grab the promise from the Deferred and attach some behavior to it.
var promise = deferred.promise; promise.then(function success(data) { console.log(data); }, function error(msg) { console.error(msg); });Finally, we perform some fake work and indicate we're done by telling the deferred:
deferred.resolve('all done!');Of course, that's not really asynchronous, so we can just fake that using Angular's $timeout service (or Javascript's setTimeout, but, prefer $timeout in Angular applications so you can mock / test it)
$timeout(function() { deferred.resolve('All done... eventually'); }, 1000);And the fun part: we can attach multiple then()s to a single promise, as well as attach then()s after the promise has resolved:
var deferred = $q.defer(); var promise = deferred.promise; // assign behavior before resolving promise.then(function (data) { console.log('before:', data); }); deferred.resolve("Oh look we're done already.") // assign behavior after resolving promise.then(function (data) { console.log('after:', data); });Now, what if some error occurred? We'll use deferred.reject(), which will cause the second argument of then() to be called. Just like callbacks.
var deferred = $q.defer(); var promise = deferred.promise; promise.then(function success(data) { console.log('Success!', data); }, function error(msg) { console.error('Failure!', msg); }); deferred.reject('We failed :(');As an alternative to passing a second argument to then(), you can chain it with a catch(), which will be called if anything goes wrong in the promise chain (more on chaining later):
promise .then(function success(data) { console.log(data); }) .catch(function error(msg) { console.error(msg); });As an aside, for longer-term processes (like uploads, long calculations, batch operations, etc), you can use deferred.notify() and the third argument of then() to give the promise's listeners a status update:
var deferred = $q.defer(); var promise = deferred.promise; promise .then(function success(data) { console.log(data); }, function error(error) { console.error(error); }, function notification(notification) { console.info(notification); } ); var progress = 0; var interval = $interval(function() { if (progress >= 100) { $interval.cancel(interval); deferred.resolve('All done!'); } progress += 10; deferred.notify(progress + '%...'); }, 100)
promise .then(doSomething) .then(doSomethingElse) .then(doSomethingMore) .catch(logError);For a simple example, this allows you to neatly separate your function calls into pure, single-purpose functions, instead of one-thing-does-all; double bonus if you can re-use those functions for multiple promise-like tasks, just like how you would chain functional methods (on lists and the like). It becomes more powerful if you use the result of a previous asynchronous to trigger a next one. By default, a chain like the one above will pass the returned object to the next then(). Example:
var deferred = $q.defer(); var promise = deferred.promise; promise .then(function(val) { console.log(val); return 'B'; }) .then(function(val) { console.log(val); return 'C' }) .then(function(val) { console.log(val); }); deferred.resolve('A');This will output the following to the console:
A B CThis is a simple example though. It becomes really powerful if your then() callback returns another promise. In that case, the next then() will only be executed once that promise resolves. This pattern can be used for serial HTTP requests, for example (where a request depends on the result of a previous one):
var deferred = $q.defer(); var promise = deferred.promise; // resolve it after a second $timeout(function() { deferred.resolve('foo'); }, 1000); promise .then(function(one) { console.log('Promise one resolved with ', one); var anotherDeferred = $q.defer(); // resolve after another second $timeout(function() { anotherDeferred.resolve('bar'); }, 1000); return anotherDeferred.promise; }) .then(function(two) { console.log('Promise two resolved with ', two); });In summary:
$q.all([promiseOne, promiseTwo, promiseThree]) .then(function(results) { console.log(results[0], results[1], results[2]); });The second variant accepts an Object of promises, allowing you to give names to those promises in your callback method (making them more descriptive):
$q.all({ first: promiseOne, second: promiseTwo, third: promiseThree }) .then(function(results) { console.log(results.first, results.second, results.third); });I would only recommend using the array notation if you can batch-process the result, i.e. if you treat the results equally. The object notation is more suitable for self-documenting code. Another utility method is $q.when(), which is useful if you just want to create a promise out of a plain variable, or if you're simply not sure if you're dealing with a promise object.
$q.when('foo') .then(function(bar) { console.log(bar); }); $q.when(aPromise) .then(function(baz) { console.log(baz); }); $q.when(valueOrPromise) .then(function(boz) { // well you get the idea. })$q.when() is also useful for things like caching in services:
angular.module('myApp').service('MyService', function($q, MyResource) { var cachedSomething; this.getSomething = function() { if (cachedSomething) { return $q.when(cachedSomething); } // on first call, return the result of MyResource.get() // note that 'then()' is chainable / returns a promise, // so we can return that instead of a separate promise object return MyResource.get().$promise .then(function(something) { cachedSomething = something }); }; });And then call it like this:
MyService.getSomething() .then(function(something) { console.log(something); });
angular.module('fooApp') .service('BarResource', function ($resource) { return $resource('api/bar/:id'); }) .service('BarService', function (BarResource) { this.getBar = function (id) { return BarResource.get({ id: id }).$promise; } });This example is a bit obscure because passing the id argument to BarResource looks a bit duplicate, but it makes sense if you've got a complex object but need to call a service with just an ID property from it. The advantage of the above is that in your controller, you know that anything you get from a Service will always be a promise object; you don't have to wonder whether it's a promise or resource result or a HttpPromise, which in turn makes your code more consistent and predictable - and since Javascript is weakly typed and as far as I know there's no IDE out there yet that can tell you what type a method returns without developer-added annotations, that's pretty important.
angular.module('WebShopApp') .controller('CheckoutCtrl', function($scope, $log, CustomerService, CartService, CheckoutService) { function calculateTotals(cart) { cart.total = cart.products.reduce(function(prev, current) { return prev.price + current.price; }); return cart; } CustomerService.getCustomer(currentCustomer) .then(CartService.getCart) // getCart() needs a customer object, returns a cart .then(calculateTotals) .then(CheckoutService.createCheckout) // createCheckout() needs a cart object, returns a checkout object .then(function(checkout) { $scope.checkout = checkout; }) .catch($log.error) });This combines getting data asynchronously (customers, carts, creating a checkout) with processing data synchronously (calculateTotals); the implementation however doesn't know or need to know whether those various services are async or not, it will just wait for the methods to complete, async or not. In this case, getCart() could fetch data from local storage, createCheckout() could perform a HTTP request to make sure the products are all in stock, etcetera. But from the consumer's point of view (the one making the calls), it doesn't matter; it Just Works. And it clearly states what it's doing, just as long as you remeber that the result of the previous then() is passed to the next. And of course it's self-documenting and concise.
describe('The Checkout controller', function () { beforeEach(module('WebShopApp')); it('should do something with promises', inject(function ($controller, $q, $rootScope) { // create mocks; in this case I use jasmine, which has been good enough for me so far as a mocking library. var CustomerService = jasmine.createSpyObj('CustomerService', ['getCustomer']); var CartService = jasmine.createSpyObj('CartService', ['getCart']); var CheckoutService = jasmine.createSpyObj('CheckoutService', ['createCheckout']); var $scope = $rootScope.$new(); var $log = jasmine.createSpyObj('$log', ['error']); // Create deferreds for each of the (promise-based) services var customerServiceDeferred = $q.defer(); var cartServiceDeferred = $q.defer(); var checkoutServiceDeferred = $q.defer(); // Have the mocks return their respective deferred's promises CustomerService.getCustomer.andReturn(customerServiceDeferred.promise); CartService.getCart.andReturn(cartServiceDeferred.promise); CheckoutService.createCheckout.andReturn(checkoutServiceDeferred.promise); // Create the controller; this will trigger the first call (getCustomer) to be executed, // and it will hold until we start resolving promises. $controller("CheckoutCtrl", { $scope: $scope, CustomerService: CustomerService, CartService: CartService, CheckoutService: CheckoutService }); // Resolve the first customer. var firstCustomer = { id: "customer 1" }; customerServiceDeferred.resolve(firstCustomer); // ... However: this *will not* trigger the 'then()' callback to be called yet; // we need to tell Angular to go and run a cycle first: $rootScope.$apply(); expect(CartService.getCart).toHaveBeenCalledWith(firstCustomer); // setup the next promise resolution var cart = { products: [{ price: 1 }, { price: 2 }] } cartServiceDeferred.resolve(cart); // apply the next 'then' $rootScope.$apply(); var expectedCart = angular.copy(cart); cart.total = 3; expect(CheckoutService.createCheckout).toHaveBeenCalledWith(expectedCart); // Resolve the checkout service var checkout = { total: 3 }; // doesn't really matter checkoutServiceDeferred.resolve(checkout); // apply the next 'then' $rootScope.$apply(); expect($scope.checkout).toEqual(checkout); expect($log.error).not.toHaveBeenCalled(); })); });As you can see, testing promise code is about ten times as long as the code itself; I don't know if / how to have the same power in less code, but, maybe there's a library out there I haven't found (or made) yet. To get full test coverage, one will have to write tests wherein all three services fail to resolve, one after the other, to make sure the error is logged. While not clearly visible in the code, the code / process actually does have a lot of branches; every promise can, after all, resolve or reject; true or false, or branch out. But, that level of testing granularity is up to you in the end. I hope this article gives people some insight into promises and how they can be used in any Angular application. I think I've only scratched the surface on the possibilities, both in this article and in the AngularJS projects I've done so far; for such a simple API and such simple concepts, the power and impact promises have on most Javascript applications is baffling. Combined with high-level functional utilities and a clean codebase, promises allow you to write your application in a clean, maintainable and easily altered fashion; add a handler, move them around, change the implementation, all these things are easy to do and comprehend if you've got promises under control. With that in mind, it's rather odd that promises were scrapped from NodeJS early on in its development in favor of the current callback nature; I haven't dived into it completely yet, but it seems it had performance issues that weren't compatible with Node's own goals. I do think it makes sense though, if you consider NodeJS to be a low-level library; there are plenty of libraries out there that add the higher-level promises API to Node (like the aforementioned Q). Another note is that I wrote this post with AngularJS in mind, however, promises and promise-like programming has been possible in the grandfather of Javascript libraries for a couple of years now, jQuery; Deferreds were added in jQuery 1.5 (January 2011). Not all plugins may be using them consistently though. Similarly, Backbone.js' Model api also exposes promises in its methods (save() etc), however what I understand it doesn't really work right alongside its model events. I might be wrong though, it's been a while. I would definitely recommend aiming for a promise-based front-end application whenever developing a new webapp, it makes the code so much cleaner, especially combined with functional programming paradigms. More functional programming patterns can be found in Reginald Braithwaite's Javascript Allongé book, free to read on LeanPub; some of those should also be useful in writing promise-based code.