Recently I needed to have the ability to cancel $http requests in our AngularJS app at La Belle Assiette. We have these large export tables, with filters, and we needed to ensure that the previous request gets canceled whenever a filter changes so that the new request results don’t get overwritten by a slower, older request.

There are already a few implementations lying around on the Internet, such as this blog post, by K. Scott Allen, or this one, by Charlee Li. However, for each implementation, I wasn’t a fan of their usability, and I did not want to add more properties to the promise implementation or to modify it.

Introduction

Of course, AngularJS already provides a way to cancel $http requests: it can take a promise as the $timeout parameter. Resolving this promise before the request response will cancel it. Let’s look at an example:

function fetchBooks() {
  const canceller = $q.defer();
  return $http.get('/api/books/', { timeout: canceller.promise });
}

However, in this code, you can notice we don’t have access to the canceller promise after we have returned the $http promise from our fetchBooks function. What we can do to solve this, is to call fetchBooks with the promise already built:

function fetchBooks(canceller) {
  return $http.get('/api/books/', { timeout: canceller.promise });
}

// Caller
const canceller = $q.defer();
const books = fetchBooks(canceller);

// Eventually cancel the promise.
canceller.resolve();

If you’ve already used cancel token from axios, a ReactJS HTTP library, you probably see a similarity here: we create a “token” (here represented by our promise), we “give” it to our API request, and then we can use that token to cancel the request.

However, this implementation still doesn’t fully answer the need we had earlier: have only one concurrent request: if you create as many “tokens” as you create requests, you’re not preventing anything to happen in parallel.

The CancelToken class

Let me present a final implementation, wrapped in a nice AngularJS service:

function CancelToken() {
  this.reset();
}

CancelToken.prototype.reset = function () {
  this._canceller = this.$q.defer();
  this.isPendingRequest = false;
};

CancelToken.prototype.activate = function () {
  this.isPendingRequest = true;
  return this._canceller.promise;
};

CancelToken.prototype.cancel = function () {
  this._canceller.resolve();
  this.reset();
};

angular
  .module('cancelTokenModule')
  .service('CancelToken', ['$q', function ($q) {
    CancelToken.prototype.$q = $q;
    return CancelToken;
  }]);

We now have a CancelToken class that we can instantiate and that will represent a request that can have at most one concurrent call. The instantiation will call the reset method that creates the internal promise (_canceller). We then modify our fetchBooks function as follows:

function fetchBooks(cancelToken) {
  return $http.get('/api/books/', {
    timeout: cancelToken ? cancelToken.activate() : void 0,
  });
}

The activate method returns the canceller promise. We can then call cancel on the cancel token instance, which will resolve the promise and reset the instance, for it to be ready to be activated again.

Full example

function fetchBooks(cancelToken) {
  return $http.get('/api/books/', {
    timeout: cancelToken ? cancelToken.activate() : void 0,
  });
}

angular
  .module('books', ['cancelTokenModule'])
  .controller('BooksList', ['$scope', 'CancelToken', function ($scope, CancelToken) {
    $scope.cancelToken = new CancelToken();

    $scope.getPage = function () {
      $scope.cancelToken.cancel();
      fetchBooks($scope.cancelToken)
        .success(function (res) {
          $scope.cancelToken.reset();
          $scope.books = res.data;
        });
    };

    $scope.getPage();
  }]);

Above is a full code example with this new CancelToken class. Notice that we cancel the token before each request, and that we reset when a request is done so that it can be re-used right after.

isPendingRequest

The CancelToken class also exposes a state isPendingRequest. This allows (if you expose your cancel token to $scope) to know if there is a pending request, to show a spinner for example:

<h1>
  Books List <i ng-show="cancelToken.isPendingRequest" class="fa fa-spinner fa-spin"></i>
</h1>

Conclusion

I believe this implementation is relatively simple and clean, and leaves only minimal cancellation related logic in the main controllers. Ideally, fetchBooks could cancel the token itself and reset it when the request is done, but I leave this as an exercise to the reader ;)

If you have any suggestion, please don’t hesitate to comment!