About a month ago, I came across an interesting problem at work where a database migration with Knex was not working properly. An example similar to the actual problem is shown below:
The first promise renames a table from
potatoes. The following promise adds the string column
name to the newly renamed table
potatoes. While there may seem to be nothing wrong here, this code resulted in an error, saying the table
potatoes did not exist.
It turns out the problem was the Promise collection itself. I had a preconceived notion that the API for
Promise.all was a function that resolved a promise array sequentially. It turns out it resolves asynchronous tasks concurrently!
There was an issue open at one point for a sequential Promise collection method, and the author of Bluebird does give a compelling argument for why
Promise.series was never added to Bluebird. Nevertheless, I wanted to solve my current issue without using callbacks. I needed a way to serially resolve an array of promises and return an array of results.
My solution was to extend Bluebird with a
Promise.series method that will take in an Iterable and return a Promise that resolves similarly to
Promise.all, but with all of the promises executed serially through using
Here’s the code I wrote for
The above implementation of series uses
Promise.reduce to return a promise that will iterate through the promise array, and resolve each promise one after the other. This function will return an array of resolved values (the starting value is the empty array, and results are pushed in as the reducer executes).
There is a caveat: for this method to work, we must wrap each promise in a function, almost like a factory. Promises must be wrapped because as soon as a promise is created, it begins execution (see
new Promise() documentation here).
Simple example below (thanks to madeofpalk on Hacker News):
This gives us the serial execution we desire, and it allows our example above to work!
So, let’s update the initial example with the new series method we have:
Presto! The first promise now resolves before the second promise transitions to pending, which means that the
potatoes table now exists and we can add columns to it. By extending Bluebird, we now have a function for those rare cases when the performance benefits of parallel execution are derailed by a need for serial execution.
A serial solution for serial people.
Edit: I’ve updated the post with a better example to more clearly demonstrate the problem I was trying to solve.
Edit 2: As a bunch of people on Hacker News have pointed out, my initial implementation of
Promise.series does not work because the promises are already executing if passed directly into
Promise.series without being wrapped in a function that returns a new Promise (essentially, a factory). I have therefore gone ahead and performed a major rewrite of the implementation of
Promise.series and the examples, with a new section explaining this. Special thanks to flattersatz and madeofpalk’s comments, and to a great article by David Atchley that helped me along the way to a better solution.