Promises in AngularJS
So what are promises?
The simplest definition of promises is given by the Promises/A+ specification:
A promise represents the eventual result of an asynchronous operation.
The two main functions performed by promises are:
- provide an interface for handling incomplete (asynchronous) operations
- error handling for the said operations
With a few nice extras derived from the above:
- chain asynchronous operations while avoiding callback hell
- a syntax that loosely resembles the
try catch finallyconstruct
Generic vs AngularJS implementation
Promise instances can either be constructed by specialized factories (the case with Angular), or by static methods on the Promise object itself.
The MDN article on this topic defines a
Promise as an object that has a few static methods such as
reject. This object is used to construct a new Promise instance.
Note that a promise will either: resolve with a value or reject with a reason.
Resolve or reject can be called at any later time, a feature which gives promises their asynchronous nature.
In Angular promises are created with the Deferred API. The
$q.defer() method will construct a new Deferred instance that can either be resolved or rejected. Note that Angular also has a
notify method that provides updates on the execution status.
The Promise instance can then be obtained from the
promise property found on the Deferred instance.
Either approach works, but Angular might change its behavior in the future to reflect the (eventually) upcoming ECMAScript 6 specification.
After construction, the Promise instance will expose a few methods such as
thenaccepts a success and an error callback function. Angular also has a notify callback.
catchonly accepts an error callback and is a shorthand for
finallywill be run regardless of the outcome of the promise.
Promises can be chained together (composed):
then function will return a derived promise passing either: the success value down the chain, or the error reason.
A nice feature of the AngularJS implementation is that if a success callback returns a promise instance as value, the chain will resume execution only after that promise is fulfilled and its value will be passed on to the next success callback.
So basically, we can nest other promises inside the success callbacks and have them interleave with the main promise chain. This is immensely powerful and enables us to build services such as the ones mentioned in the Use cases below.
The $http service
In AngularJS, the
$http service depends heavily on promises. Each $http call will return a special kind of promise instance that has two extra methods attached:
error. These methods will receive parameters such as
config from the service, which enable us to work more easily with requests.
However, it's the $http interceptors where promises really shine. Interceptors allow us to hook into http requests and responses, and react to them or modify their outcome. Here are a few examples:
Pre-processing of requests:
- Displaying a progress indicator.
- Changing the request
Content-Typeheader, useful when performing asynchronous file uploads for example.
- Authentication: each time a 401 http response is encountered the user could be redirected to the login page.
- Global error handling: each time a certain kind of error is received from the backend (such as the user not having enough credits to perform an action) a notice could pop up.
Interceptors can be chained together, since they can return a promise.
Here are two custom services that rely on promises.
In larger client-side applications you will most probably need some form of ACLs. ACL stands for Access Control Lists and represents a mechanism for authorizing users to certain parts of the application based on the user group they belong to.
Reduced to its simplest form, ACL could be expressed as a list of user groups, with each group entry containing a list of allowed application routes.
At Zooku, we use a hybrid ACL service for the upcoming Control Panel. This service obtains its routes in two ways:
- synchronous: some of them are predefined and do not change while the application is running.
- asynchronous: some are requested from the server-side and change when the user's active group changes.
Promises enable us to have a common interface for checking both synchronous and asynchronous requested routes.
By creating an initial promise that always resolves, we can then chain a
getUserGroup and a
getAcls request to it. These two requests are performed in the bootstrapping phase of the application or when the user changes credentials, such as logging in.
After the active user group and the asynchronous ACLs are set, we can then proceed to perform the route
check itself, which will also be chained to the previous requests.
In our case subsequent route checks will not request the user group nor the ACL routes again (they will be cached for the active session), but by using promises we created an uniform interface that works regardless of the application session state.
A well designed validation service has the potential to truly help the user and eliminate a lot of frustration.
While HTML5 introduced a few helpful attributes for dealing with validation, employing them in a large application can quickly result in views overloaded with validation attributes, or with attributes that are out of sync with the backend.
For both Zooku and Sendmachine, we developed a Validation service that has a few nice features:
- Uses a chain of rules to validate a field, each rule providing the user with hints about the current state of the field.
- Requests validation rules from the server-side, which enabled us to use the same rules on both the server and the client application.
While most rules perform synchronous checks, like checking if a required field is filled-in, others perform asynchronous checks, like checking with the backend if an email is already registered.
This is another case there promises prove to be very useful and enable us to perform checks on a field regardless of their nature.
The logic is very similar to the ACL service: an initial Promise instance that always resolves is created, and promises that perform further validations are chained to it.
These are just a few use cases where promises play a significant role. More documentation and code samples can be found in the resources below.