How do I sequentially chain promises with angularjs $q?
Asked Answered
M

7

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;
Mercie answered 6/9, 2014 at 21:6 Comment(0)
M
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;
Mercie answered 6/9, 2014 at 21:6 Comment(5)
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).Icebox
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?Icebox
How would I know when all promises in chain are succesfully resolved? chain.then or $q.all(chain).then seem not to workChloromycetin
@Zbynek, at the very end you could use chain.then(function () { console.log('all resolved'); });Mercie
Here is a similar solution in a codepen. 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).Winer
A
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.

Aristocrat answered 8/9, 2014 at 18:25 Comment(10)
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.Icebox
@FlavorScape, good catch. It's good to know there are people out there checking this stuff.Aristocrat
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()Aristocrat
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.Aristocrat
@Mercie pointed out, making it a factory chains it properly, if you miss that nuance, the reduce executes the functions immediately while building the chain.Icebox
@FlavorScape, read the "The Collection Kerfuffle" section here and see if you still think that.Aristocrat
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.Vassell
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.Vassell
@pulkitsinghal, you might like to read my question Building a promise chain recursively in javascript - memory considerations, which actually starts from a slightly different perspective, but attracted two answers that cover the ground pretty well.Aristocrat
Thanks @Aristocrat for a concise solution to this problem.Cavitation
M
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;
Mercie answered 6/9, 2014 at 21:6 Comment(5)
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).Icebox
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?Icebox
How would I know when all promises in chain are succesfully resolved? chain.then or $q.all(chain).then seem not to workChloromycetin
@Zbynek, at the very end you could use chain.then(function () { console.log('all resolved'); });Mercie
Here is a similar solution in a codepen. 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).Winer
S
5
var when = $q.when();

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

    })(i); 
}
Ship answered 2/6, 2017 at 20:1 Comment(1)
Very elegant !You saved me day :)Heavensent
M
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; });

Minded answered 22/7, 2016 at 15:42 Comment(3)
Does this work if you have in an indeterminate number of promises that need to be resolved sequentially?Grouper
I'm pretty sure you can, I think that's exactly what is happening in redgeoff's answerMinded
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.Grouper
V
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());
Voltameter answered 22/3, 2017 at 13:8 Comment(0)
O
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)
Outsize answered 31/10, 2014 at 15:38 Comment(2)
Could you elaborate on the benefit of angular.bind in this scenario?Vassell
I used bind to create promise-returning functions that do not need any arguments. Thus can they be just used like .then(foo).then(bar)Outsize
D
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);
    });
  };
}
Deberadeberry answered 24/3, 2015 at 5:21 Comment(4)
This looks interesting, I shall try to refactor after writing a million unit tests on my plate for today...Icebox
This is definitely a much simpler approach because it is easier to read the code later on.Softcover
$q.serial is a non existent doc pageHalfcaste
The link point to some shady website that try to install something.Meier

© 2022 - 2024 — McMap. All rights reserved.