Thinkster

Promises

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.