Executing promises serially with [].reduce
Recently at $WORK, we were writing a data migration script in node that needed to make a couple hundred requests. The first attempt was just to wrap everything up in a Promise.all:
const rp = require('request-promise-native');
const urls = [
'url1',
'url2',
// ...,
'url300'
];
Promise.all(urls.map((url) => rp.get(url)
.then(sendRelatedRequests)));
However, the internal server we were talking to wasn't able to handle all of the requests concurrently, and since the subsequent logic would also send a few more requests of its own, we ended up taking down the server because we were spawning all the promises at the same time, and since Promises execute once they're made, that means all the requests were starting off at roughly the same time.
So, for our second pass, we decided we wanted to only send one request
at a time, lining up all of our requests serially, since we know that
when the server finishes responding to our nth request, it should be
ready to handle the (n+1)th request. One way to accomplish this is
with a big long chain of .thens, as by the time we're in a .then, we're guaranteed that its preceding promise is completed. And one way we can construct that chain is with a reduce:
urls.reduce(
(acc, url) => acc.then(() => rp.get(url).then(sendRelatedRequests)),
Promise.resolve()
);
[].reduce takes two arguments: the reducing function, and the
initial value. We need to start with a Promise, because our reduce
function assumes that the accumulator acc has a .then on it.
For the reducing function, we have an accumular, and a url. Each time,
acc is the existing serial chain of promises, and we add another
.then on to it. The important part is that the function in the
.then handler is not immediately creating the promise, because
that would mean the request is immediately sent. Instead, passing the
function expression means the Promise isn't created until the .then
handler is invoked, and since the .then handler is invoked until its
preceding Promise is complete, we get our serial behavior.
Also, since the requests don't care about each other, we don't need to use the arguments that are coming from the previous promise, so the function expression doesn't use its arguments.
The one last catch (hoho) is that if any of the rp.get(url) promises
fail, then all of the subsequent .thens are skipped, as the promise
flow dictates that it should jump to a .catch handler, if one
exists. So, to guarantee that we do make all the requests that we
wanted to, we need to add a catch handler to each of the promises in
the chain.
urls.reduce(
(acc, url) => acc.then(() => rp.get(url)
.then(sendRelatedRequests)
.catch(console.error)),
Promise.resolve()
);