Promises in AngularJS are provided by the built-in $q
service. They
provide a way to execute asynchronous functions in series by registering them
with a promise object.
{info}
Promises have made their way into native JavaScript as part of the ES6
specification. The angular $q
service provides an interface that closely
resembles this new API so porting code to ES6 should be a breeze.
The Setup:
<html>
<head>
<title>Promise fun</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/angular.min.js"></script>
<script src="app.js"></script>
</head>
<body ng-app="app">
</body>
</html>
function getData($timeout, $q) {
return function() {
var defer = $q.defer()
// simulated async function
$timeout(function() {
}, 2000)
return defer.promise
}
}
angular.module('app', [])
.factory('getData', getData)
.run(function(getData) {
var promise = getData()
})
For simplicity's sake, we'll use the $timeout()
service to simulate an
asynchronous function. Practically speaking AJAX calls using the $http
service are some of the most common scenarios where promises are used.
The Deferred Object
A deferred object is simply an object that exposes a promise as well as the
associated methods for resolving that promise. It is constructed using the
$q.deferred()
function and exposes three main methods: resolve()
,
reject()
, and notify()
. The associated promise object can be accessed
via the promise
property.
function getData($timeout, $q) {
return function() {
var defer = $q.defer()
// simulated async function
$timeout(function() {
defer.resolve('data received!')
}, 2000)
return defer.promise
}
}
Here we create a new deferred object, then return its promise
property. We
also execute our asnyc function and after it completes, we resolve the deferred
object. The parameter of the resolve()
function will be passed to the
callback function.
The Promise Object
Now that we've obtained a Promise object (defer.promise
), let's register a callback
function that'll be executed after the async function completes.
.run(function(getData) {
var promise = getData()
.then(function(string) {
console.log(string)
})
})
Now when you refresh the page, you should see "data received!" printed to the console after 2 seconds.
Rejecting a Promise
We've seen how to successfully resolve a promise, but what happens if the async function
fails? Rather than using the resolve()
method, we can call the reject()
method if
something goes wrong.
function getData($timeout, $q) {
return function() {
var defer = $q.defer()
// simulated async function
$timeout(function() {
if(Math.round(Math.random())) {
defer.resolve('data received!')
} else {
defer.reject('oh no an error! try again')
}
}, 2000)
return defer.promise
}
}
The second parameter of the then()
method is an optional error handling callback
function that'll be called if and only if the promise is rejected.
.run(function(getData) {
var promise = getData()
.then(function(string) {
console.log(string)
}, function(error) {
console.error(error)
})
})
Now if you repeatedly refresh the page, you should see the error message about half the time!
{info}
Multiple callback functions can be registered to the same promise object
by making different calls to the then()
method. The functions will be
executed in the order in which they were registered.
Using the $q Constructor
The $q
service itself is also a function that allows you to quickly convert
a callback based asynchronous function into a promise based solution.
function getData($timeout, $q) {
return function() {
// simulated async function
return $q(function(resolve, reject) {
$timeout(function() {
if(Math.round(Math.random())) {
resolve('data received!')
} else {
reject('oh no an error! try again')
}
}, 2000)
})
}
}
This method accomplishes the same thing as manually creating the deferred
object-- which way you choose is up to preference and whether you want to
notify()
the calling code.
The finally() Method
One of the guarantees promises make is that either the success or the error
callback will be invoked, but never both. What happens if you need to ensure
a specific function executes regardless of the result of the promise? You can
do this by registering that function on the promise using the finally()
method. This can be really useful for reseting code to a known state.
.run(function(getData) {
var promise = getData()
.then(function(string) {
console.log(string)
}, function(error) {
console.error(error)
})
.finally(function() {
console.log('Finished at:', new Date())
})
})
You should see the current Data+time printed to the console regardless of whether the promise was resolved or rejected.
Promise Chaining
One of the most powerful features of promises is the ability to chain them together. This allows the data to flow through the chain and be manipulated and mutated at each step.
Let's take a look at a basic example to get started.
function getData($timeout, $q) {
return function() {
// simulated async function
return $q(function(resolve, reject) {
$timeout(function() {
resolve(Math.floor(Math.random() * 10))
}, 2000)
})
}
}
On page refresh, you should now see a random integer between 0-9 printed out.
To begin chaining, we have to modify the callback function to return a value.
.run(function(getData) {
var promise = getData()
.then(function(num) {
console.log(num)
return num * 2
})
})
Now, we can chain another callback to our promise object using the
then()
function which will be executed after the first callback
function returns. The value passed to the second callback function
will be the random number * 2:
.run(function(getData) {
var promise = getData()
.then(function(num) {
console.log(num)
return num * 2
})
.then(function(num) {
console.log(num) // = random number * 2
})
})
While this is a simple example, it illustrates a very powerful concept. Furthermore, rather than return simple values from a promise callback, you can return new promises. The promise chain will "pause" until the returned promise resolves. This allows you to chain together multiple async function calls (such as multiple requests to a server).
Wrap-Up
Promises already play a huge role in the Angular framework and with the release of ES6, are poised to play an increasing role in JavaScript in the future. While they may seem confusing at first (especially chaining), they provide an intuitive and clean interface for dealing with asynchronous code and are thus a fundamental building block of modern day JavaScript.