$q.serial – execute promises serially in AngularJS.

Hey AngularJS devs! Let’s talk about executing async tasks serially with promises and how $q.serial can help.

First, we notice .then allows our success/fail callbacks to return a promise. $q treats success/fail callbacks that return promises specially. We’ll look at two examples to see how $q treats success/fail functions that return promises differently from those that don’t.

Normal success function:

$q.when()  
.then(function () { console.log("foo"); })
.then(function () { console.log("bar"); });
foo
bar

Success function that returns a promise:

$q.when()
.then(function () { 
  console.log("foo"); 
  var bazDeferred = $q.defer();
  var bazPromise = bazDeferred.promise;
  return bazPromise;
})
.then(function () { console.log("bar"); });
foo  

As expected, our first example logs “foo” then “bar”. However, our second example only logs “foo”. $q does not continue executing the chain of promises until bazPromise has been resolved. So “bar” is never logged.

This is good! $q allows us to execute chained promises sequentially by returning promises in our success functions.

Let’s see this $q feature in action:

$q.when()
.then(function () {
  var deferred = $q.defer();
  console.log("foo"); 
  $timeout(function () { deferred.resolve(); }, 5000);
  return deferred.promise;
})
.then(function () { console.log("bar"); });
foo  
//5 seconds
bar

Great, so now we see how we can execute promises sequentially. How do we pass data through to the next promise? Easy, we pass arguments to .resolve() or .reject().

$q.when()
.then(function () {
  var deferred = $q.defer();
  console.log("foo"); 
  $timeout(function () { deferred.resolve("bar"); }, 5000);
  return deferred.promise;
})
.then(function (data) { console.log(data); })
foo  
//5 seconds
bar

Ok, so we’ve only chained two sequential promises together. What if I want to chain 5, 10, or 100? Can we generalize this construct? A little harder, but sure we can!

function serial(tasks) {
  var prevPromise;
  angular.forEach(tasks, function (task) {
    //First task
    if (!prevPromise) { 
      prevPromise = task(); 
    } else {
      prevPromise = prevPromise.then(task); 
    }
  });
  return prevPromise;
}

var task1 = function () {
  var task1Deferred = $q.defer();
  $timeout(function () {
    task1Deferred.resolve("foo");
  }, 5000);
  return task1Deferred.promise;
};

var task2 = function (data) { console.log(data); };

serial([task1, task2]);
foo
//after 5 seconds
bar  

Great, task1 is executed. Then task2 is executed after task1Promise is resolved.

Hold up, I just used a new word “task.” IMO we in the AngularJS community don’t use the word “task” as often as we should. So let me define how I imagine “task.” In plain english a task is a unit of work to be done. With reference to AngularJS, I often of a task as a function that returns a promise.

Why? Because a function performs a unit of work (or at least kicks off a unit of work). And a promise let’s me know the status of the task (in progress, succeeded, failed). Knowing the status of a task is very useful!

The term isn’t perfect, but it helps me organize my thoughts when thinking about promises. So let’s stick with it.

function washDishesTask() {
  var deferred = $q.defer();
  $timeout(function () { 
    washDishes(); 
    deferred.resolve();
  }, 1000);
  return deferred.promise;
}

Now that we have serial() and have defined “tasks,” let’s finish creating $q.serial by adding handy error checking and decorating $q.

angular.module("qImproved", [])
.config(function ($provide) {
  $provide.decorator("$q", function ($delegate) {
    //Helper method copied from q.js.
    var isPromiseLike = function (obj) { return obj && angular.isFunction(obj.then); }

    /*
     * @description Execute a collection of tasks serially.  A task is a function that returns a promise
     *
     * @param {Array.<Function>|Object.<Function>} tasks An array or hash of tasks.  A tasks is a function
     *   that returns a promise.  You can also provide a collection of objects with a success tasks, failure task, and/or notify function
     * @returns {Promise} Returns a single promise that will be resolved or rejected when the last task
     *   has been resolved or rejected.
     */
    function serial(tasks) {
      //Fake a "previous task" for our initial iteration
      var prevPromise;
      var error = new Error();
      angular.forEach(tasks, function (task, key) {
        var success = task.success || task;
        var fail = task.fail;
        var notify = task.notify;
        var nextPromise;

        //First task
        if (!prevPromise) {
          nextPromise = success();
          if (!isPromiseLike(nextPromise)) {
            error.message = "Task " + key + " did not return a promise.";
            throw error;
          }
        } else {
          //Wait until the previous promise has resolved or rejected to execute the next task
          nextPromise = prevPromise.then(
            /*success*/function (data) {
              if (!success) { return data; }
              var ret = success(data);
              if (!isPromiseLike(ret)) {
                error.message = "Task " + key + " did not return a promise.";
                throw error;
              }
              return ret;
            },
            /*failure*/function (reason) {
              if (!fail) { return $delegate.reject(reason); }
              var ret = fail(reason);
              if (!isPromiseLike(ret)) {
                error.message = "Fail for task " + key + " did not return a promise.";
                throw error;
              }
              return ret;
            },
            notify);
        }
        prevPromise = nextPromise;
      });

      return prevPromise || $delegate.when();
    }

    $delegate.serial = serial;
    return $delegate;
  });
});

Now that we have the full $q.serial API, let’s investigate its power. $q.serial is most useful when you have a tasks that are expensive, tie up resources, and are not time sensitive.

At Hurdlr we encountered this exact criteria the other day. We build a mobile app. Internet tends to be less reliable on phones than on computers. So we try to minimize server calls.

One way we minimize server calls is by eagerly loading bulk data. So depending on module, user’s config, etc. we execute a set of tasks that load bulk data. We don’t want to execute all the tasks at once because it can tie up more database resources and phone resources when caching than we’d like.

var tasks = [];
if (user.config.foo) {
    tasks.push(retrieveFooData);
}
if (module.bar) {
    tasks.push(retrieveBarData);
}
$q.serial(tasks);

Ok, so we’ve got 99% of the functionality we need. But I should mention that $q.serial supports reject and notify also. Oh, and $q.serial can accept a hash instead of an array.

var task1 = function () {
  var deferred = $q.defer();
  console.log("one");
  $timeout(function () { deferred.notify(); });
  $timeout(function () { deferred.reject(); }, 5000);
  return deferred.promise;
};
var task2 = {
  success: function () {
    console.log("two");
    return $q.when();
  }, fail: function () {
    console.log("fail two");
    return $q.reject();
  }, notify: function () {
    console.log("notified two");
  }
};

$q.serial({ one: task1, two: task2 });
one
notified two
//5 seconds
fail two

So that’s $q.serial, a generalized way to execute async tasks serially in AngularJS.

Steven Wexler
Follow me!

Steven Wexler

Software Engineer at Hurdlr
I'm a software engineer living in Washington D.C. and working at a startup called Hurdlr. I primarily develop in C#, Javascript, and SQL. And I play around with Python and Scala on the side. I enjoy participating as an active member on StackOverflow and working on side projects.Check out my exceptions.js framework!
Steven Wexler
Follow me!

27 thoughts on “$q.serial – execute promises serially in AngularJS.

  1. Hi,

    First and formost, thank you for the awesome solution! This is exactly what I need!

    Quesiton: Is it possible to return an array/hash of result after all the tasks have been completely sequentially? Just like $q.all, I hope the execution result can be returned in the success callback so i can do something like this:

    $q.serial([task1, task2]).then(function(responses){
    location.assign(“task1detail/” + responses[0].id)
    },function(errors){
    alertErrors(errors); //extract the error messages
    })

    thank you very much for your contribution!

  2. In the first serial function, you return prevTask but that variable doesn’t exist. Did you mean to return prevPromise?

  3. Hi Steven!

    I want to say, that you wrote really nice article! Thanks for it!

    Also, I have an question: how I have an array of promises and how I can invoke them one after another using your decorator?

  4. Hi, thank you for your post.

    I would like to use your decorator but it seems like I am doing something wrong.

    I have this code:

    //In my controller:
    $scope.exportSingleItem = function(theComponent) {
    return cbomService.exportComponent(theComponent);
    };
    $scope.exportSelected = function() {
    var arrComponents = $scope.getSelectedItems();
    var promises = [];
    for (var i = 0; i < arrComponents.length; i++) {
    var current = arrComponents[i];
    var promise = $scope.exportSingleItem(current);
    promises.push(promise);
    }
    $q.serial(promises);
    };

    //In cbomService service:
    this.exportComponent = function(theComponent) {
    return $http.post(appConfig.API_URL + 'someBackendAPI', "=" + JSON.stringify(theComponent))
    .then(function(response) {
    if (typeof response.data === 'object') {
    var backendResponse = response.data;
    if (!backendResponse.ErrorThrown) {
    /* Some other stuff with response */
    return response.data;
    } else {
    alertify.alert(backendResponse.ResponseDescription).set('modal', true);
    return $q.reject(response.data);
    }
    } else {
    // invalid response
    return $q.reject(response.data);
    }
    }, function(response) {
    // something went wrong
    return $q.reject(response.data);
    });
    };

    I have understood that $http.post is returning a promise, so I am pushing it to an array of promises to be used in $q.serial method, but it seems like that promise returned by $http.post has no success method, so that I am getting the error:

    TypeError: success is not a function

    Thank you in advance.

    1. Hey, sorry for your trouble! It looks like you’re passing in promises into $q.serial. Instead you should pass in functions.

      var promise = function () { return $scope.exportSingleItem(current); };

      Note, the variable name is now confusing. Your “promise” is really a function, not a promise. So you may want to rename that variable.

      1. Thanks!! It did the job!

        This is my resulting code:

        $scope.exportSelected = function() {
        $activityIndicator.startAnimating();
        var arrComponents = $scope.getSelectedItems();

        var arrPromiseConstructors = [];
        angular.forEach(arrComponents, function(component, key) {
        var promiseConstructor = function() {
        return $scope.exportSingleItem(component);
        }
        arrPromiseConstructors.push(angular.copy(promiseConstructor));
        });

        $q.serial(arrPromiseConstructors).finally(function(data) {
        var theSelectedEntity = cbomService.getBySIF($scope.sif);
        angular.copy(theSelectedEntity, $scope.assembly);
        $activityIndicator.stopAnimating();
        $timeout(function() {
        alertify.success(‘Components exported to APQM successfully.’);
        }, 100);
        });
        };

  5. Hey this is exactly what I needed. I was wondering if you had some unit tests to test the $q.serial module? This would help to understand some of the code.

  6. @steven, Thank you for the article!

    I have a question: For q-serial and other such examples alike … I find that my promise wrapped http requests (tasks if you will) get fired immediately (sequentially but immediately) and that’s not what I want. I want my api server to be pinged nice and slow, one request after another.

    All the example including yours leverage $timeout but my tasks don’t and that’s probably the only difference … I thought $timeout was meant as a placeholder for a real async call but now I think its the only thing helping add any real delays between the sequences … what i want is for resolved promises to result in the next $http call and not rely on $timeout … I feel like I’m barking up the wrong tree, can you point me in the right direction?

    1. @pulkitsinghal sorry for getting back to you so late! From how I’m reading your question, you should be able to ping the server sequentially and not immediately without $timeout.

      function task1() { return $http.get(“/foo”); }
      function task2() { return $http.get(“/bar”); }
      $q.serial([
      task1,
      //task 2 will execute after task 1 has gotten data back from the server.
      task2
      ]);

      If you want to insert delays between your requests you can use $timeout.

      function task1() { return $http.get(“/foo”); }
      function task2() { return $http.get(“/bar”); }
      function delay() { return $timeout(function () {}, 10 * 1000); }
      $q.serial([
      task1,
      delay,
      //task 2 will execute after task 1 has gotten data back from the server + 10 seconds.
      task2
      ]);

  7. q-serial seems to be exactly what I need; however, it isn’t clear to me how to parameterize promises in the sequence. My first task is to retrieve a list of users, then using that list of users, I extract user IDs and retrieve additional data. I want to do this in order and depth. I tried creating a task array, pushing task1GetUsers into it, then within that task onsuccess I push additional tasks; however, these new dependent tasks (functions) will not execute just the first one. Is this the right approach?

    1. Hi yanon, your approach seems correct to me. I think you’ll want something like:

      function retreiveUsers() { /*doStuff*/ }

      function task1GetUsers() { /*get info for user 1*/ }

      function task2GetUsers() { /*get info for user 2 */ }

      $q.serial([retrieveUsers, task1GetUsers, task2GetUsers]);

  8. Nice way to execute promises in a sequential way.
    Anyway, it seems like the tasks stop running when a promise is rejected. Is it normal?
    How to proceed to execute every success taks functions regardless of if the previous was reject?

    1. Hey Petitoheme. Not executing downstream promises when a prior fails is common. Usually, to continue the promise chain you’lll implement an errorCallback. See http://www.codeducky.org/promises-danger-angular-noop/. With $q.serial you can also do this:

      var task1 = function () { throw new error(“uh oh”); });
      var task2 = { success: function () { }, fail: function (reason) { return “fixed” });

      $q.serial([task1, task2]);

      In the above example task2.fail will be invoked.

      1. I made a slight modification. I wanted it to work just like $q.all. Instead of passing the results from task to task, I wanted it to return an array of results. Here is what I am using.

        .config(function ($provide) {
        $provide.decorator(“$q”, function ($delegate) {
        //Helper method copied from q.js.
        var isPromiseLike = function (obj) { return obj && angular.isFunction(obj.then); }

        /*
        * @description Execute a collection of tasks serially. A task is a function that returns a promise
        *
        * @param {Array.|Object.} tasks An array or hash of tasks. A tasks is a function
        * that returns a promise. You can also provide a collection of objects with a success tasks, failure task, and/or notify function
        * @returns {Promise} Returns a single promise that will be resolved or rejected when the last task
        * has been resolved or rejected.
        */
        function serial(tasks) {
        //Fake a “previous task” for our initial iteration
        var prevPromise;
        var error = new Error();
        var results = [];

        var lastTask = function() {
        var endPromise = $delegate.defer();
        endPromise.resolve(results);
        return endPromise.promise;
        }
        tasks = tasks.concat(lastTask);

        angular.forEach(tasks, function (task, key) {
        var success = task.success || task;
        var fail = task.fail;
        var notify = task.notify;
        var nextPromise;

        //First task
        if (!prevPromise) {
        nextPromise = success();
        if (!isPromiseLike(nextPromise)) {
        error.message = “Task ” + key + ” did not return a promise.”;
        throw error;
        }
        } else {
        //Wait until the previous promise has resolved or rejected to execute the next task
        nextPromise = prevPromise.then(
        /*success*/function (result) {
        results.push(result);
        if (!success) return results;

        var ret = success(result);
        if (!isPromiseLike(ret)) {
        error.message = “Task ” + key + ” did not return a promise.”;
        throw error;
        }
        return ret;
        },
        /*failure*/function (reason) {
        if (!fail) { return $delegate.reject(reason); }
        var ret = fail(reason);
        if (!isPromiseLike(ret)) {
        error.message = “Fail for task ” + key + ” did not return a promise.”;
        throw error;
        }
        return ret;
        },
        notify);
        }
        prevPromise = nextPromise;
        });

        return prevPromise || $delegate.when();
        }

        $delegate.serial = serial;
        return $delegate;
        });
        });

Leave a Reply