Async Functions and AngularJs 1.X Do Not Mix

January 17, 2020

Recently, I was refactoring an AngularJS 1.x project and wrote the following code:

// DO NOT USE THIS CODE, IT BREAKS ANGULARJS!
// Combining $http with async functions does not work in AngularJS 1.X
async function updateItem(item) {
  const { data } = await $http.put(`/items/${item.id}`, item)
  return data
}

While the above might seem innocent enough, it breaks Angular’s digest cycle. When the promise fulfills, you will not see the page update.

What you should do instead?

Do not use await/async with $http. Instead, use the old promise style, with .then().

// Use promise.then() instead of async/await
function updateItem(item) {
  return $http.put(`/items/${item.id}`, item).then(({ data }) => data)
}

While the above might seem like completely reasonable and modern JavaScript, attempting to use it will break Angular’s digest cycle.

Where does it break?

Let’s look at what the browser does when it executes the async/await code

// DO NOT USE THIS CODE, IT BREAKS ANGULARJS!
// Combining $http with async functions does not work in AngularJS 1.X

// This function:
async function updateItem(item) {
  const { data } = await $http.put(`/items/${item.id}`, item)  return data
}


// Is equivalent to the following code:
function updateItem(item) {
  return Promise.resolve($http.put(`/items/${item.id}`, item)).then((value) => {    const { data } = value
    return data
  })
}

As you can see, the original $q promise returned from $http.put() is wrapped in a new Promise.resolve(). This means AngularJS can no longer track when the promise settles.

The issue comes up when you try to use the async version of updateItem() in a controller:

function SomeCtrl($scope, initialItem) {
  $scope.item = initialItem

  this.onClick = function() {
    updateItem($scope.item)
      .then(updatedItem => ($scope.item = updatedItem))  }
}

The async function implementation of updateItem() would break the highlighted line. The changed $scope.item variable will not be reflected in the DOM, or watchers, until a random digest cycle executes.

The reason is that AngularJS cannot know when a plain promise resolves.

AngularJS has special wrappers for browser primitives—$timeout, $interval and its own Promise library $q. AngularJS needs to wrap these asynchronous interfaces in order to track when they complete, and run a $rootScope.$digest() cycle.

When we used an async function, we’re in the same predicament as if we’d used setTimeout() directly instead of $timeout()—there is no way for AngularJS to track when the execution of the async function completed.

To make the async function work in our controller, we would need to re-wrap it with $q.resolve():

function SomeCtrl($q, $scope, initialItem) {
  $scope.item = initialItem

  this.onClick = function() {
    $q.resolve(updateItem($scope.item)).then(      updatedItem => ($scope.item = updatedItem)    )  }
}

Or we could use another async function and a $apply() around our controller property assignment:

function SomeCtrl($scope, initialItem) {
  $scope.item = initialItem

  this.onClick = async function() {    const updatedItem = await updateItem($scope.item)    $scope.$apply(() => {      $scope.item = updatedItem    })  }}

We end up having to manually wrap any effects of async function code with $scope.$apply(), or wrap Promises with $q.resolve(). This makes it not worth using async/await in the first place. This is unfortunate when we need to coordinate multiple async tasks, as the async/await interfaces make that much nicer.

In conclusion

Modern async/await functions are great, and its tempting to want to use them when refactoring old code. However, it’s not worth the hassle in AngularJS 1.X projects. We’ll have to stick to the $q promise interfaces instead.


Profile picture

Written by Ivan Tanev
Twitter / GitHub