Promises

We already mentioned at the beginning of the chapter that CPS is not the only way to write asynchronous code. In fact, the JavaScript ecosystem provides alternatives to the traditional callback pattern. One of these in particular is receiving a lot of momentum, especially now that it is going to be part of the ECMAScript 6 specification (also known as ES6 or Harmony), the upcoming version of the JavaScript language. We are talking, of course, about promises, and in particular about those implementations that follow the Promises/A+ specification (https://promisesaplus.com).

In very simple terms, promises are an abstraction that allow an asynchronous function to return an object called a promise, which represents the eventual result of the operation. In the promises jargon, we say that a promise is pending when the asynchronous operation is not yet complete, it's fulfilled when the operation successfully completes, and rejected when the operation terminates with an error. Once a promise is either fulfilled or rejected, it's considered settled.

To receive the fulfillment value or the error (reason) associated with the rejection, we can use the then() method of the promise. The following is its signature:

Where onFulfilled() is a function that will eventually receive the fulfillment value of the promise, and onRejected() is another function that will receive the reason of the rejection (if any). Both functions are optional.

To have an idea of how Promises can transform our code, let's consider the following:

Promises allow to transform this typical CPS code into a better structured and more elegant code, such as the following:

One crucial property of the then() method is that it synchronously returns another promise. If any of the onFulfilled() or onRejected() functions return a value x, the promise returned by the then() method will be as follows:

This feature allows us to build chains of promises, allowing easy aggregation and arrangement of asynchronous operations in several configurations. Also, if we don't specify an onFulfilled() or onRejected() handler, the fulfillment value or rejection reasons are automatically forwarded to the next promise in the chain. This allows us, for example, to automatically propagate errors across the whole chain until caught by an onRejected() handler. With a promise chain, sequential execution of tasks suddenly becomes a trivial operation:

The following diagram provides another perspective on how a promise chain works:

What is a promise?

Another important property of promises is that the onFulfilled() and onRejected() functions are guaranteed to be invoked asynchronously, even if we resolve the promise synchronously with a value, as we did in the preceding example, where we returned the string done in the last then() function of the chain. This behavior shields our code against all those situations where we could unintentionally release Zalgo, making our asynchronous code more consistent and robust with no effort.

Now comes the best part. If an exception is thrown (using the throw statement) from the onFulfilled() or onRejected() handler, the promise returned by the then() method will automatically reject with the exception as the rejection reason. This is a tremendous advantage over CPS, as it means that with promises, exceptions will propagate automatically across the chain, and that the throw statement is not an enemy anymore.

In Node.js, and in general in JavaScript, there are several libraries implementing the Promises/A+ specifications. The following are the most popular:

What really differentiates them is the additional set of features they provide on top of the Promises/A+ standard. The standard, in fact, defines only the behavior of the then() method and the promise resolution procedure, but it does not specify other functionalities, for example, how a promise is created from a callback-based asynchronous function.

In our examples, we will try to use the set of API implemented by the ES6 promises, as they will be natively available in JavaScript without the support of any external library. Luckily, the preceding list of libraries are gradually adapting to support the ES6 API, so using any one of them should not force us into any strong implementation lock-in as far as we use only the feature set of the ES6 standard.

For reference, here is the list of the APIs currently provided by the ES6 promises:

In Node.js, and in general in JavaScript, there are only a few libraries supporting promises out-of-the-box. Most of the time, in fact, we have to convert a typical callback-based function into one that returns a promise; this is also known as promisification.

Fortunately, the callback conventions used in Node.js allow us to create a reusable function that we can utilize to promisify any Node.js style API. We can do this easily by using the constructor of the Promise object. Let's then create a new function called promisify() and include it into the utilities.js module (so we can use it later in our web spider application):

The preceding function returns another function called promisified(), which represents the promisified version of the callbackBasedApi given in the input. This is how it works:

After a little bit of necessary theory, we are now ready to convert our web spider application to use promises. Let's start directly from version 2, the one downloading in sequence the links of a web page.

In the spider.js module, the very first step required is to load our promises implementation (we will use it later) and promisify the callback-based functions that we plan to use:

Now, we can start converting the download() function:

We can see straightaway how elegant some sequential code implemented with promises is; we simply have an intuitive chain of then() functions. The final return value of the download() function is the promise returned by the last then() invocation in the chain. This makes sure that the caller receives a promise that fulfills with body only after all the operations (request, mkdirp, writeFile) have completed.

Next, it's the turn of the spider() function:

The important thing to notice here is that we also registered an onRejected() function for the promise returned by readFile(), to handle the case when a page was not already downloaded (file does not exist). Also, it's interesting to see how we were able to use throw to propagate the error from within the handler.

Now that we have converted our spider() function as well, we can modify its main invocation as follows:

Note how we used, for the first time, the syntactic sugar catch to handle any error situation originated from the spider() function. If we look again at all the code we have written so far in this section, we would be pleasantly surprised by the fact that we didn't include any error propagation logic like we would be forced to do by using callbacks. This is clearly an enormous advantage, as it greatly reduces the boilerplate in our code and the chances of missing any asynchronous error.

Now, the only missing bit to complete the version 2 of our web spider application is the spiderLinks() function, which we are going to see in a moment.

The web spider code so far was mainly an overview of what promises are and how they are used, demonstrating how simple and elegant it is to implement a sequential execution flow using promises. However, the code we considered so far involves only the execution of a known set of asynchronous operations. So, the missing piece that will complete our exploration of sequential execution flows is to see how we can implement an iteration using promises. Again, the spiderLinks() function of web spider version 2 is a perfect example to show that.

Let's add the missing piece to the code we wrote so far:

To iterate asynchronously over all the links of a web page, we had to dynamically build a chain of promises:

This way, at the end of the loop, the promise variable will contain the promise of the last then() invocation in the loop, so it will resolve only when all the promises in the chain have been resolved.

With this, we completely converted our web spider version 2 to use promises. We should now be able to try it out again.

Another execution flow that becomes trivial with promises is the parallel execution flow. In fact, all that we need to do is use the built-in Promise.all() helper that creates another promise, which fulfills only when all the promises received in an input are fulfilled. That's essentially a parallel execution because no order between the various promises' resolutions is enforced.

To demonstrate this, let's consider version 3 of our web spider application, the one downloading all the links of a page in parallel. Let's update the spiderLinks() function again to implement a parallel flow, using promises:

Trivially, the pattern consists in starting the spider() tasks all at once into the elements.map() loop, which also collects all their promises. This time, in the loop, we are not waiting for the previous download to complete before starting a new one, all the download tasks are started in the loop at once, one after the other. Afterwards, we leveraged the Promise.all()method, which returns a new promise that will be fulfilled when all the promises in the array are fulfilled. In other words, it fulfills when all the download tasks have completed; exactly what we wanted.

Unfortunately, the ES6 Promise API does not provide a way to implement a limited parallel control flow natively, but we can always rely on what we learned about limiting the concurrency with plain JavaScript. In fact, the pattern we implemented inside the TaskQueue class can be easily adapted to support tasks that return a promise. This can be done trivially by modifying the next() method:

So now, instead of handling the task with a callback, we simply invoke then() on the promise it returns. The rest of the code is practically identical to the old version of TaskQueue.

Now, we can go back to the spider.js module, modifying it to support our new version of the TaskQueue class. First, we make sure to define a new instance of TaskQueue:

Then, it's the turn of the spiderLinks() function again. The change here is also pretty straightforward:

There are a couple of things in the preceding code that merit our attention:

Version 4 of the web spider application using promises should now be ready to be tried out. We might notice once again how the download tasks now run in parallel, with a concurrency limit of 2.