I saw this example implementation of Promise.all - which runs all promises in parallel - Implementing Promise.all
Note that the functionality I am looking for is akin to Bluebird's Promise.mapSeries .html
I am making an attempt at creating Promise.series, I have this which seems to work as intended (it actually is totally wrong, don't use it, see answers):
Promise.series = function series(promises){
return new Promise(function(resolve,reject){
const ret = Promise.resolve(null);
const results = [];
promises.forEach(function(p,i){
ret.then(function(){
return p.then(function(val){
results[i] = val;
});
});
});
ret.then(function(){
resolve(results);
},
function(e){
reject(e);
});
});
}
Promise.series([
new Promise(function(resolve){
resolve('a');
}),
new Promise(function(resolve){
resolve('b');
})
]).then(function(val){
console.log(val);
}).catch(function(e){
console.error(e.stack);
});
However, one potential problem with this implementation is that if I reject a promise, it doesn't seem to catch it:
Promise.series([
new Promise(function(resolve, reject){
reject('a'); // << we reject here
}),
new Promise(function(resolve){
resolve('b');
})
]).then(function(val){
console.log(val);
}).catch(function(e){
console.error(e.stack);
});
does anyone know why the error doesn't get caught and if there is a way to fix this with Promises?
According to a comment, I made this change:
Promise.series = function series(promises){
return new Promise(function(resolve,reject){
const ret = Promise.resolve(null);
const results = [];
promises.forEach(function(p,i){
ret.then(function(){
return p.then(function(val){
results[i] = val;
},
function(r){
console.log('rejected');
reject(r); // << we handle rejected promises here
});
});
});
ret.then(function(){
resolve(results);
},
function(e){
reject(e);
});
});
}
but this still doesn't work as expected...
I saw this example implementation of Promise.all - which runs all promises in parallel - Implementing Promise.all
Note that the functionality I am looking for is akin to Bluebird's Promise.mapSeries http://bluebirdjs.com/docs/api/mapseries.html
I am making an attempt at creating Promise.series, I have this which seems to work as intended (it actually is totally wrong, don't use it, see answers):
Promise.series = function series(promises){
return new Promise(function(resolve,reject){
const ret = Promise.resolve(null);
const results = [];
promises.forEach(function(p,i){
ret.then(function(){
return p.then(function(val){
results[i] = val;
});
});
});
ret.then(function(){
resolve(results);
},
function(e){
reject(e);
});
});
}
Promise.series([
new Promise(function(resolve){
resolve('a');
}),
new Promise(function(resolve){
resolve('b');
})
]).then(function(val){
console.log(val);
}).catch(function(e){
console.error(e.stack);
});
However, one potential problem with this implementation is that if I reject a promise, it doesn't seem to catch it:
Promise.series([
new Promise(function(resolve, reject){
reject('a'); // << we reject here
}),
new Promise(function(resolve){
resolve('b');
})
]).then(function(val){
console.log(val);
}).catch(function(e){
console.error(e.stack);
});
does anyone know why the error doesn't get caught and if there is a way to fix this with Promises?
According to a comment, I made this change:
Promise.series = function series(promises){
return new Promise(function(resolve,reject){
const ret = Promise.resolve(null);
const results = [];
promises.forEach(function(p,i){
ret.then(function(){
return p.then(function(val){
results[i] = val;
},
function(r){
console.log('rejected');
reject(r); // << we handle rejected promises here
});
});
});
ret.then(function(){
resolve(results);
},
function(e){
reject(e);
});
});
}
but this still doesn't work as expected...
Share Improve this question edited May 23, 2017 at 10:29 CommunityBot 11 silver badge asked Jun 1, 2016 at 21:07 Alexander MillsAlexander Mills 100k165 gold badges531 silver badges908 bronze badges 12 | Show 7 more comments4 Answers
Reset to default 4The promise returned by then
in the forEach
loop does not handle potential errors.
As pointed out in a comment by @slezica, try to use reduce
rather than forEach
, this chains all promises together.
Promise.series = function series(promises) {
const ret = Promise.resolve(null);
const results = [];
return promises.reduce(function(result, promise, index) {
return result.then(function() {
return promise.then(function(val) {
results[index] = val;
});
});
}, ret).then(function() {
return results;
});
}
Keep in mind that the promises are already "running" at that point though. If you truly want to run your promises in series, you should adjust your function and pass in an array of functions that return promises. Something like this:
Promise.series = function series(providers) {
const ret = Promise.resolve(null);
const results = [];
return providers.reduce(function(result, provider, index) {
return result.then(function() {
return provider().then(function(val) {
results[index] = val;
});
});
}, ret).then(function() {
return results;
});
}
This is a common misunderstanding of how promises work. People want there to be a sequential equivalent to the parallel Promise.all
.
But promises don't "run" code, they're mere return values one attaches completion callbacks to.
An array of promises, which is what
Promise.all
takes, is an array of return values. There's no way to "run" them in sequence, because there's no way to "run" return values.
Promise.all
just gives you one promise representing many.
To run things in sequence, start with an array of things to run, i.e. functions:
let p = funcs.reduce((p, func) => p.then(() => func()), Promise.resolve());
or an array of values to run a function over:
let p = values.reduce((p, val) => p.then(() => loadValue(val)), Promise.resolve());
Read up on reduce here.
Update: Why Promises don't "run" code.
Most people intuitively understand that callbacks don't run in parallel.
(Workers aside,) JavaScript is inherently event-driven and single-threaded, and never runs in parallel. Only browser functions, e.g. fetch(url)
can truly do work in parallel, so an "asynchronous operation" is a euphemism for a synchronous function call that returns immediately, but is given a callback (e.g. where resolve
would be called) that will be called later.
Promises don't change this reality. They hold no inherent asynchronous powers (*), beyond what can be done with callbacks. At their most basic, they're a (very) neat trick to reverse the order in which you need to specify callbacks.
*) Technically speaking, promises do have something over callbacks, which is a micro-task queue in most implementations, which just means promises can schedule things at the tail of the current crank of the JavaScript event-loop. But that's still not vastly different, and a detail.
EDIT 2
According to your edit, you're looking for Promise.mapSeries
as provided by bluebird. You've given us a bit of a moving target, so this edit changes direction from my previous answer because the mapSeries
function works very differently than just executing a collection of Promises in serial order.
// mock bluebird's mapSeries function
// (Promise [a]) -> (a -> b) -> (Promise [b])
Promise.prototype.mapSeries = function mapSeries(f) {
return this.then(reducek (ys=> x=> k=> {
let value = f(x);
let next = x=> k([...ys, x]);
return value instanceof Promise ? value.then(next) : next(value);
}) ([]));
};
Just to get a top-level idea of how this would be used
// given: (Promise [a]) and (a -> b)
// return: (Promise [b])
somePromiseOfArray.mapSeries(x=> doSomething(x)); //=> somePromiseOfMappedArray
This relies on a small reducek
helper which operates like a normal reduce
except that the callback receives an additional continuation argument. The primary advantage here is that our reducing procedure has the option of being asynchronous now. The computation will only proceed when the continuation is applied. This is defined as a separately because it's a useful procedure all on its own; having this logic inside of mapSeries
would make it overly complicated.
// reduce continuation helper
// (a -> b -> (a -> a)) -> a-> [b] -> a
const reducek = f=> y=> ([x, ...xs])=> {
if (x === undefined)
return y;
else
return f (y) (x) (y => reducek (f) (y) (xs));
};
So you can get a basic understanding of how this helper works
// normal reduce
[1,2,3,4].reduce((x,y)=> x+y, 0); //=> 10
// reducek
reducek (x=> y=> next=> next(x+y)) (0) ([1,2,3,4]); //=> 10
Next we have two actions that we'll use in our demos. One that is completely synchronous and one that returns a Promise. This demonstrates that mapSeries
can also work on iterated values that are Promises themselves. This is the behaviour defined by bluebird.
// synchronous power
// Number -> Number -> Number
var power = x=> y=> Math.pow(y,x);
// asynchronous power
// Number -> Number -> (Promise Number)
var powerp = x=> y=>
new Promise((resolve, reject)=>
setTimeout(() => {
console.log("computing %d^%d...", y, x);
if (x < 10)
resolve(power(x)(y));
else
reject(Error("%d is just too big, sorry!", x));
}, 1000));
Lastly, a small helper used to facilitate logging in the demos
// log promise helper
const logp = p=>
p.then(
x=> console.log("Done:", x),
err=> console.log("Error:", err.message)
);
Demo time! Here I'm going to dogfood my own implementation of mapSeries
to run each demo in sequential order!.
Because mapSeries
excepts to be called on a Promise, I kick off each demo with Promise.resolve(someArrayOfValues)
// demos, map each demo to the log
Promise.resolve([
// fully synchronous actions map/resolve immediately
()=> Promise.resolve([power(1), power(2), power(3)]).mapSeries(pow=> pow(2)),
// asynchronous items will wait for resolve until mapping the next item
()=> Promise.resolve([powerp(1), powerp(2), powerp(3)]).mapSeries(pow=> pow(2)),
// errors bubble up nicely
()=> Promise.resolve([powerp(8), powerp(9), powerp(10)]).mapSeries(pow=> pow(2))
])
.mapSeries(demo=> logp(demo()));
Go ahead, run the demo now
// reduce continuation helper
// (a -> b -> (a -> a)) -> a-> [b] -> a
const reducek = f=> y=> ([x, ...xs])=> {
if (x === undefined)
return y;
else
return f (y) (x) (y => reducek (f) (y) (xs));
};
// mock bluebird's mapSeries function
// (Promise [a]) -> (a -> b) -> (Promise [b])
Promise.prototype.mapSeries = function mapSeries(f) {
return this.then(reducek (ys=> x=> k=>
(x=> next=>
x instanceof Promise ? x.then(next) : next(x)
) (f(x)) (x=> k([...ys, x]))
) ([]));
};
// synchronous power
// Number -> Number -> Number
var power = x=> y=> Math.pow(y,x);
// asynchronous power
// Number -> Number -> (Promise Number)
var powerp = x=> y=>
new Promise((resolve, reject)=>
setTimeout(() => {
console.log("computing %d^%d...", y, x);
if (x < 10)
resolve(power(x)(y));
else
reject(Error("%d is just too big, sorry!", x));
}, 1000));
// log promise helper
const logp = p=>
p.then(
x=> console.log("Done:", x),
err=> console.log("Error:", err.message)
);
// demos, map each demo to the log
Promise.resolve([
// fully synchronous actions map/resolve immediately
()=> Promise.resolve([power(1), power(2), power(3)]).mapSeries(pow=> pow(2)),
// asynchronous items will wait for resolve until mapping the next item
()=> Promise.resolve([powerp(1), powerp(2), powerp(3)]).mapSeries(pow=> pow(2)),
// errors bubble up nicely
()=> Promise.resolve([powerp(8), powerp(9), powerp(10)]).mapSeries(pow=> pow(2))
])
.mapSeries(f=> logp(f()));
EDIT
I'm reapproaching this problem as a series of promises should be considered like a chain or composition of promises. Each resolve promise will feed it's value to the next promise.
Per @Zhegan's remarks, it makes more sense for the series
function to take an array of promise creators, otherwise there's no way to guarantee the promises would run in serial. If you pass an array of Promises, each promise will immediately run its executor and start doing work. Thus, there's no way that the work of Promise 2 could depend on the completed work of Promise 1.
Per @Bergi's remarks, my previous answer was a little weird. I think this update makes things a little more consistent.
Promise series without error
// ([(a-> (Promise b)), (b-> (Promise c)]), ...]) -> a -> (Promise c)
Promise.series = function series(tasks) {
return x=>
tasks.reduce((a,b)=> a.then(b), Promise.resolve(x));
};
// a -> [a] -> (Promise [a])
var concatp = x=> xs=>
new Promise((resolve, reject)=>
setTimeout(() => {
console.log(xs, x);
if (xs.length < 3)
resolve(xs.concat([x]));
else
reject(Error('too many items'));
}, 250));
var done = (x)=> console.log('done:', x);
var err = (e)=> console.log('error:', e.message);
Promise.series([concatp(3), concatp(6), concatp(9)]) ([]) .then(done, err);
// [] 3
// [ 3 ] 6
// [ 3, 6 ] 9
// done: [ 3, 6, 9 ]
Promise series with an error
// ([(a-> (Promise b)), (b-> (Promise c)]), ...]) -> a -> (Promise c)
Promise.series = function series(tasks) {
return x=>
tasks.reduce((a,b)=> a.then(b), Promise.resolve(x));
};
// a -> [a] -> (Promise [a])
var concatp = x=> xs=>
new Promise((resolve, reject)=>
setTimeout(() => {
console.log(xs, x);
if (xs.length < 3)
resolve(xs.concat([x]));
else
reject(Error('too many items'));
}, 250));
var done = (x)=> console.log('done:', x);
var err = (e)=> console.log('error:', e.message);
Promise.series([concatp(3), concatp(6), concatp(9), concatp(12)]) ([]) .then(done, err);
// [] 3
// [ 3 ] 6
// [ 3, 6 ] 9
// [ 3, 6, 9 ] 12
// error: too many items
@forrert's answer is pretty much spot on
Array.prototype.reduce
is a bit confusing, so here is a version without reduce. Note that in order to actually run promises in series we must wrap each promise in a provider function and only invoke the provider function inside the Promise.series function. Otherwise, if the promises are not wrapped in functions, the promises will all start running immediately and we cannot control the order in which they execute.
Promise.series = function series(providers) {
const results = [];
const ret = Promise.resolve(null);
providers.forEach(function(p, i){
ret = ret.then(function(){
return p().then(function(val){
results[i] = val;
});
});
});
return ret.then(function(){
return results;
});
}
the equivalent functionality using reduce:
Promise.series = function series(providers) {
const ret = Promise.resolve(null);
const results = [];
return providers.reduce(function(result, provider, index) {
return result.then(function() {
return provider().then(function(val) {
results[index] = val;
});
});
}, ret).then(function() {
return results;
});
}
you can test both functions using this:
Promise.series([
function(){
return new Promise(function(resolve, reject){
setTimeout(function(){
console.log('a is about to be resolved.')
resolve('a');
},3000);
})
},
function(){
return new Promise(function(resolve, reject){
setTimeout(function(){
console.log('b is about to be resolved.')
resolve('b');
},1000);
})
}
]).then(function(results){
console.log('results:',results);
}).catch(function(e){
console.error('Rejection reason:', e.stack || e);
});
note that it's not a good idea to attach functions, or otherwise alter, native global variables, like we just did above. However, also note that the native library authors also left us with native libraries that are wanting in functionality :)
reduce()
instead offorEach
, chaining them bythen
– salezica Commented Jun 1, 2016 at 21:21Promise.series
gets already created promises that are running right after they are created. – Eugene Commented Jun 1, 2016 at 21:30new Promise(executor)
theexecutor
function is invoked upon creation (moreover synchronously). InsidePromise.series
you can only wait for promises fulfillment/rejection. – Eugene Commented Jun 1, 2016 at 21:38then()
doesn't "start" a promise. BothPromise.all([$.get("foo.html"), $.get("bar.html")])
andPromise.series([$.get("foo.html"), $.get("bar.html")])
would immediately start 2 AJAX requests in parallel and then await their results. If you want the next request to start after the previous one completed, you need to givePromise.series
an array of "promise factories": functions that create (and thus "start") a promise. – Mattias Buelens Commented Jun 1, 2016 at 21:40Promise.series
and execute them in series. Passing array of promises just does not make sense for me - it works almost the same asPromise.all
– Eugene Commented Jun 1, 2016 at 21:41