17

In the promise library Q, you can do the following to sequentially chain promises:

var items = ['one', 'two', 'three'];
var chain = Q();
items.forEach(function (el) {
  chain = chain.then(foo(el));
});
return chain;

however, the following doesn't work with $q:

var items = ['one', 'two', 'three'];
var chain = $q();
items.forEach(function (el) {
  chain = chain.then(foo(el));
});
return chain;
redgeoff
  • 3,163
  • 1
  • 25
  • 39

7 Answers7

37

Redgeoff, your own answer is the way I used to translate an array into a chained series of promises.

The emergent de facto pattern is as follows :

function doAsyncSeries(arr) {
    return arr.reduce(function (promise, item) {
      return promise.then(function(result) {
        return doSomethingAsync(result, item);
      });
    }, $q.when(initialValue));
}

//then
var items = ['x', 'y', 'z'];
doAsyncSeries(items).then(...);

Notes:

  • .reduce is raw javascript, not part of a library.
  • result is the previous async result/data and is included for completeness. The initial result is initialValue. If it's not necessary to pass `result, then simply leave it out.
  • adapt $q.when(initialValue) depending on which promise lib you use.
  • in your case, doSomethingAsync is foo (or what foo() returns?) - in any case, a function.

If you are like me, then the pattern will look, at first sight, like an impenetrable cludge but once your eye becomes attuned, you will start to regard it as an old friend.

Edit

Here's a demo, designed to demonstrate that the pattern recommended above does in fact execute its doSomethingAsync() calls sequentially, not immediately while building the chain as suggested in the comments below.

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • 1
    The original answer was not syntactically correct. I fixed that. Also, what should initialValue be set to? Like the answer above, this will fire all of them simultaneously. – FlavorScape Mar 19 '15 at 23:54
  • @FlavorScape, good catch. It's good to know there are people out there checking this stuff. – Roamer-1888 Mar 20 '15 at 08:57
  • `initialValue` appears as `result` in the first iteration of the `reduce()` loop. It's value depends on the application. If `doSomethingAsync()` does not need the previous result passed to it, then the reduction initializer would simplify to `$q.when()` – Roamer-1888 Mar 20 '15 at 09:06
  • And no, the `doSomethingAsync()` calls are not simultaneous. The `reduce(...)` process builds a `.then()` chain immediately, but its execution is sequential in exactly the same way as writing out `doSomethingAsync(...).then(...).then(...)` longhand. – Roamer-1888 Mar 20 '15 at 09:13
  • @redgeoff pointed out, making it a factory chains it properly, if you miss that nuance, the reduce executes the functions immediately while building the chain. – FlavorScape Apr 06 '15 at 18:42
  • @FlavorScape, read the "The Collection Kerfuffle" section [here](http://taoofcode.net/promise-anti-patterns/) and see if you still think that. – Roamer-1888 Apr 06 '15 at 19:31
  • For roughly 7000 items in an array (i know i know background server side job would be better) I ran into `net::ERR_INSUFFICIENT_RESOURCES` some 2 minutes into it so I wonder why that would happen even when I'm using this to throttle execution to be serial rather than parallel. – pulkitsinghal Jun 17 '15 at 16:16
  • Actually it gets through it all and then pops that up at the very end, I wonder if its just related to chrome debug console not being able to do some of its own introspection magic and not to the app. – pulkitsinghal Jun 17 '15 at 16:23
  • 1
    @pulkitsinghal, you might like to read my question [Building a promise chain recursively in javascript - memory considerations](http://stackoverflow.com/questions/29925948/), which actually starts from a slightly different perspective, but attracted two answers that cover the ground pretty well. – Roamer-1888 Jun 17 '15 at 18:43
  • 1
    Thanks @Roamer-1888 for a concise solution to this problem. – Deniz Jul 16 '15 at 07:03
34

Simply use the $q.when() function:

var items = ['one', 'two', 'three'];
var chain = $q.when();
items.forEach(function (el) {
  chain = chain.then(foo(el));
});
return chain;

Note: foo must be a factory, e.g.

function setTimeoutPromise(ms) {
  var defer = $q.defer();
  setTimeout(defer.resolve, ms);
  return defer.promise;
}

function foo(item, ms) {
  return function() {
    return setTimeoutPromise(ms).then(function () {
      console.log(item);
    });
  };
}

var items = ['one', 'two', 'three'];
var chain = $q.when();
items.forEach(function (el, i) {
  chain = chain.then(foo(el, (items.length - i)*1000));
});
return chain;
redgeoff
  • 3,163
  • 1
  • 25
  • 39
  • 4
    This does not work. It executes all of them simultaneously. I know this because I perform a series of requests that take about 500 MS. watching my network traffic, they all go out concurrently (but in order). – FlavorScape Mar 19 '15 at 23:45
  • 1
    Ah, ok making it a factory makes it so that it does not execute immediately in the call stack when we are building the chain, right? – FlavorScape Mar 20 '15 at 00:50
  • How would I know when all promises in chain are succesfully resolved? `chain.then` or `$q.all(chain).then` seem not to work – Zbynek Oct 06 '16 at 11:47
  • @Zbynek, at the very end you could use `chain.then(function () { console.log('all resolved'); });` – redgeoff Dec 23 '16 at 15:25
  • 1
    Here is a similar solution in a [codepen](https://codepen.io/pilotbean/pen/MWgdVzv). Essentially uses the same approach as @redgeoff's, but in a nice reusable function, and with the addition of returning the results of all the promises in an array (like $q.all() does). – John Barton Sep 29 '19 at 11:57
5
var when = $q.when();

for(var i = 0; i < 10; i++){
    (function() {
         chain = when.then(function() {
        return $http.get('/data');
      });

    })(i); 
}
pabloRN
  • 866
  • 10
  • 19
4

In perhaps a simpler manner than redgeoff's answer, if you don't need it automated, you can chain promises using $q.when() combined with .then() as shown in the beginning of this post. return $q.when() .then(function(){ return promise1; }) .then(function(){ return promise2; });

Community
  • 1
  • 1
Matthias
  • 3,160
  • 2
  • 24
  • 38
  • Does this work if you have in an indeterminate number of promises that need to be resolved sequentially? – Tony Brasunas Apr 04 '19 at 23:40
  • I'm pretty sure you can, I think that's exactly what is happening in [redgeoff's answer](https://stackoverflow.com/a/25704747/5272567) – Matthias Apr 09 '19 at 00:22
  • yes, it does! I got it to work. I might just be slow, but I think the explanations of the loop on this page aren't as clear as they could be so it took me a while to figure it out. Might make a clarifying answer here if I have the time. But yours *was* helpful. – Tony Brasunas Apr 10 '19 at 00:58
4

Having this:

let items = ['one', 'two', 'three'];

One line (well, 3 for readability):

return items
    .map(item => foo.bind(null, item))
    .reduce($q.when, $q.resolve());
Jean-Baptiste Martin
  • 1,399
  • 1
  • 10
  • 19
2

I prefer preparing functions that will return promises using angular.bind (or Function.prototype.bind) and then linking them into a chain using reduce shortcut. For example

// getNumber resolves with given number
var get2 = getNumber.bind(null, 2);
var get3 = getNumber.bind(null, 3);
[get2, get3].reduce(function (chain, fn) {
   return chain.then(fn);
}, $q.when())
.then(function (value) {
   console.log('chain value =', value);
}).done();
// prints 3 (the last value)
gleb bahmutov
  • 1,801
  • 15
  • 10
2

Your answer is correct. However, I thought I'd provide an alternative. You may be interested in $q.serial if you find yourself serially chaining promises often.

var items = ['one', 'two', 'three'];
var tasks = items.map(function (el) {
  return function () { foo(el, (items.length - i)*1000)); });
});

$q.serial(tasks);

function setTimeoutPromise(ms) {
  var defer = $q.defer();
  setTimeout(defer.resolve, ms);
  return defer.promise;
}

function foo(item, ms) {
  return function() {
    return setTimeoutPromise(ms).then(function () {
      console.log(item);
    });
  };
}
Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
Steven Wexler
  • 16,589
  • 8
  • 53
  • 80