Working With Promises In AngularJS

Working with Angular (and JavaScript in general) means you really need to understand asynchronous code. The default way of handling this in Angular is to use promises - no more messy callbacks.

A promise is basically an object with a then method that represents an operation whose result may be unknown. Whoever has access to the promise can use the then method to attach callbacks in order to be notified when the operation completes.

Handling Errors in Promise Chains

While promises are great, they also make it rather easy to swallow errors. Using promises means we don’t need to handle errors if we are returning the promise to the caller. This usually makes a lot of sense, for example we might make a request to the server to fetch some data - it is unlikely that we know how the UI should handle a failure at the request level, but the calling code should know how to handle this - and should therefore handle the failure.

onClick = (userId) ->
  userService
    .fetchUser(userId)
    .then (user) -> $scope.user = user

In this example, no error handler is attached to the promise returned by fetchUser. If the operation fails the error will simply disappear. A simple way to handle this would be to alert the error to the user.

handleSuccess = (user) ->
  $scope.user = user

handleError = (error) ->
  $window.alert error

onClick = (userId) ->
  userService
    .fetchUser(userId)
    .then(handleSuccess, handleError)

Re-throwing Errors

Let’s take a look at what the userService might look like.

app.service "userService", (httpService) ->
  @fetchUser = (id) ->
    httpService
      .get("/api/users/#{id}")
      .then((response) -> response.data)

  @

The fetchUser function does not need to register a callback, since it is returning the promise to the caller - the caller is expected to handle any possible errors.

However, what if we wanted to log any errors that happen during this step? (I’m just using console.log here, but this could be a logging service like HoneyBadger.) We could possibly implement logging like this:

app.service "userService", (httpService) ->
  logError = (error) ->
    console.log error
    null

  @fetchUser = (id) ->
    httpService
      .get("/api/users/#{id}")
      .then ((response) -> response.data), logError

  @

This implementation will cause massive headaches - if an error occurs it will be logged (as expected), but then the promise will be resolved with the null value being returned by the logError function! In order to propagate the error up the chain we need to return a rejected promise (instead of the null value we are currently returning).

app.service "userService", (httpService, $q) ->
  logError = (error) ->
    console.log error
    $q.reject(error)

  @fetchUser = (id) ->
    httpService
      .get("/api/users/#{id}")
      .then ((response) -> response.data), logError

  @

Now the error will be logged and the original caller will still receive a rejected promise, which will trigger the error callback (and display the error to the user in this case).

More features of $q

The $q service has a few more features that are very useful when dealing with asynchronous code. For example, we might want to show a loading indicator to the user when we trigger the request, but we need to ensure that the loading indicator is always hidden when the promise completes (so both for failure and success). We can do this with the finally function.

onClick = (userId) ->
  $scope.loading = true
  userService
    .fetchUser(userId)
    .then(handleSuccess, handleError)
    .finally ->
      $scope.loading = false

You can find all the code on Plunker. Happy coding.