Chapter 4. Asynchronous Control Flow Patterns with ES2015 and Beyond

In the previous chapter, we learned how to deal with asynchronous code using callbacks and how they can have a bad impact on our code, generating issues such as callback hell. Callbacks are the building blocks of asynchronous programming in JavaScript and in Node.js, but over the years, other alternatives have emerged. Those alternatives are more sophisticated in order to be able to deal with asynchronous code in ways that are more convenient.

In this chapter, we are going to explore some of the most famous alternatives, promises and generators. We will also explore async await, an innovative syntax that will be available in JavaScript as part of the release of ECMAScript 2017.

We will see how these alternatives can simplify the way we deal with asynchronous control flows. Finally, we will compare all these approaches in order to understand all the pros and cons of each of them and be able to wisely choose the approach that best suits the requirements of our next Node.js project.

Promise

We mentioned in the previous chapters that Continuation Passing Style (CPS) is not the only way to write asynchronous code. In fact, the JavaScript ecosystem provides interesting alternatives to the traditional callback pattern. One of the most famous alternatives is promise, which is getting more and more attention, especially now that it is part of ECMAScript 2015 and has been natively available in Node.js since version 4.

What is a promise?

In very simple terms, promise is an abstraction that allows a function to return an object called promise, which represents the eventual result of an asynchronous 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:

promise.then([onFulfilled], [onRejected]) 

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

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

asyncOperation(arg, (err, result) => { 
  if(err) { 
    //handle error 
  } 
  //do stuff with result 
}); 

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

asyncOperation(arg) 
  .then(result => { 
    //do stuff with result 
  }, err => { 
    //handle error 
  });

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:

  • Fulfill with x if x is a value
  • Fulfill with the fulfillment value of x if x is a promise or a thenable
  • Reject with the eventual rejection reason of x if x is a promise or a thenable

Note

A thenable is a promise-like object with a then() method. This term is used to indicate a promise that is foreign to the particular promise implementation in use.

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:

asyncOperation(arg) 
  .then(result1 => { 
    //returns another promise 
    return asyncOperation(arg2); 
  }) 
  .then(result2 => { 
    //returns a value 
    return 'done'; 
  }) 
  .then(undefined, err => { 
    //any error in the chain is caught here 
  }); 

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 (see Chapter 2, Node.js Essential Patterns), 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) in the onFulfilled() or onRejected() handler, the promise returned by the then() method will automatically reject, with the exception thrown 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 finally usable.

Historically, there have been many different implementations of promise libraries, and most of the time they were not compatible between each other, meaning that it was not possible to create thenable chains between promise objects coming from libraries that were using different promise implementations.

The JavaScript community worked very hard to sort out this limitation and these efforts lead to the creation of the Promises/A+ specification. This specification details the behavior of the then method, providing an interoperable base, which makes promise objects from different libraries able to work with each other out of the box.

Note

For a detailed description of the Promises/A+ specification, you can refer to the official website, https://promisesaplus.com.

Promises/A+ implementations

In JavaScript, and also in Node.js, there are several libraries implementing the Promises/A+ specification. 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. As we said, the standard defines 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 use the set of APIs implemented by the ES2015 promises, as they have been natively available in Node.js since version 4 without the support of any external libraries.

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

Constructor (new Promise(function(resolve, reject) {})): This creates a new promise that fulfills or rejects based on the behavior of the function passed as an argument. The arguments of the constructor are explained as follows:

  • resolve(obj): This will resolve the promise with a fulfillment value, which will be obj if obj is a value. It will be the fulfillment value of obj if obj is a promise or a thenable.
  • reject(err): This rejects the promise with the reason err. It is a convention for err to be an instance of Error.

Static methods of the Promise object:

  • Promise.resolve(obj): This creates a new promise from a thenable or a value.
  • Promise.reject(err): This creates a promise that rejects with err as the reason.
  • Promise.all(iterable): This creates a promise that fulfills with an iterable of fulfillment values when every item in the iterable object fulfills, and rejects with the first rejection reason if any item rejects. Each item in the iterable object can be a promise, a generic thenable, or a value.
  • Promise.race(iterable): This returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.

Methods of a promise instance:

  • promise.then(onFulfilled, onRejected): This is the essential method of a promise. Its behavior is compatible with the Promises/A+ standard we described before.
  • promise.catch(onRejected): This is just syntactic sugar for promise.then(undefined, onRejected).

Note

It is worth mentioning that some promise implementations offer another mechanism to create new promises, called deferreds. We are not going to describe it here, because it's not part of the ES2015 standard, but if you want to know more, you can read the documentation for Q (https://github.com/kriskowal/q#using-deferreds) or When.js (https://github.com/cujojs/when/wiki/Deferred).

Promisifying a Node.js style function

In JavaScript, not all the asynchronous functions and libraries support promises out-of-the-box. Most of the time, we have to convert a typical callback-based function into one that returns a promise; this process 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):

module.exports.promisify = function(callbackBasedApi) { 
  return function promisified() { 
    const args = [].slice.call(arguments); 
    return new Promise((resolve, reject) => {        //[1] 
      args.push((err, result) => {                   //[2] 
        if(err) { 
          return reject(err);                        //[3] 
        } 
        if(arguments.length <= 2) {                  //[4] 
          resolve(result); 
        } else { 
          resolve([].slice.call(arguments, 1)); 
        } 
      }); 
      callbackBasedApi.apply(null, args);            //[5] 
    }); 
  } 
}; 

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:

  1. The promisified() function creates a new promise using the Promise constructor and immediately returns it to the caller.
  2. In the function passed to the Promise constructor, we make sure to pass to callbackBasedApi, a special callback. As we know that the callback always comes last, we simply append it to the argument list (args) provided to the promisified() function.
  3. In the special callback, if we receive an error, we immediately reject the promise.
  4. If no error is received, we resolve the promise with a value or an array of values, depending on how many results are passed to the callback.
  5. Finally, we simply invoke the callbackBasedApi with the list of arguments we have built.

Note

Most of the promise implementations already provide, out-of-the-box, some sort of helper to convert a Node.js style API to one returning a promise. For example, Q has Q.denodeify() and Q.nbind(), Bluebird has Promise.promisify(), and When.js has node.lift().

Sequential execution

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 the links of a web page in sequence.

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:

const utilities = require('./utilities'); 
 
const request = utilities.promisify(require('request')); 
const mkdirp = utilities.promisify(require('mkdirp')); 
const fs = require('fs'); 
const readFile = utilities.promisify(fs.readFile); 
const writeFile = utilities.promisify(fs.writeFile); 

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

function download(url, filename) { 
  console.log(`Downloading ${url}`); 
  let body; 
  return request(url) 
    .then(response => { 
      body = response.body; 
      return mkdirp(path.dirname(filename)); 
    }) 
    .then(() => writeFile(filename, body)) 
    .then(() => { 
      console.log(`Downloaded and saved: ${url}`); 
      return body; 
    }); 
} 

The important thing to notice here is that we also registered an onRejected() function for the promise returned by readFile() to handle cases where a page has not been 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:

spider(process.argv[2], 1) 
  .then(() => console.log('Download complete')) 
  .catch(err => console.log(err)); 

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

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

Sequential iteration

So far, the web spider codebase 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 until now only involves 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:

function spiderLinks(currentUrl, body, nesting) { 
  let promise = Promise.resolve(); 
  if(nesting === 0) { 
    return promise; 
  } 
  const links = utilities.getPageLinks(currentUrl, body); 
  links.forEach(link => { 
    promise = promise.then(() => spider(link, nesting - 1)); 
  }); 
 
  return promise; 
} 

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

  1. First, we defined an "empty" promise, resolving to undefined. This promise is just used as a starting point to build our chain.
  2. Then, in a loop, we updated the promise variable with a new promise obtained by invoking then() on the previous promise in the chain. This is actually our asynchronous iteration pattern using 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.

Sequential iteration – the pattern

To conclude this section on sequential execution, let's extract the pattern to iterate over a set of promises in sequence:

let tasks = [ /* ... */ ] 
let promise = Promise.resolve(); 
tasks.forEach(task => { 
  promise = promise.then(() => { 
    return task(); 
  }); 
}); 
promise.then(() => { 
  //All tasks completed 
}); 

An alternative to using the forEach() loop is the reduce() function, allowing an even more compact code:

let tasks = [ /* ... */ ] 
let promise = tasks.reduce((prev, task) => { 
  return prev.then(() => { 
    return task(); 
  }); 
}, Promise.resolve()); 
 
promise.then(() => { 
  //All tasks completed 
}); 

As always, with simple adaptations of this pattern, we could collect all the tasks' results in an array; we could implement a mapping algorithm, or build a filter, and so on.

Note

Pattern (sequential iteration with promises):

This pattern dynamically builds a chain of promises using a loop.

Parallel execution

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(). This helper function 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, which downloads all the links in page in parallel. Let's update the spiderLinks() function again to implement a parallel flow, using promises:

function spiderLinks(currentUrl, body, nesting) { 
  if(nesting === 0) { 
    return Promise.resolve(); 
  } 
 
  const links = utilities.getPageLinks(currentUrl, body); 
  const promises = links.map(link => spider(link, nesting - 1)); 
 
  return Promise.all(promises); 
} 

The pattern here consists of starting the spider() tasks all at once in 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 leverage 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; this is exactly what we wanted.

Limited parallel execution

Unfortunately, the ES2015 Promise API does not provide a native way to limit the number of concurrent tasks, 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 easily be done by modifying the next() method:

next() { 
  while(this.running < this.concurrency && this.queue.length) { 
    const task = this.queue.shift(); 
    task().then(() => { 
      this.running--; 
      this.next(); 
    }); 
    this.running++; 
  } 
} 

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

Let's go back to the spider.js module, and modify it to support our new version of the TaskQueue class. First, we make sure to define a new instance of TaskQueue:

const TaskQueue = require('./taskQueue'); 
const downloadQueue = new TaskQueue(2); 

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

function spiderLinks(currentUrl, body, nesting) { 
  if(nesting === 0) { 
    return Promise.resolve(); 
  } 
 
  const links = utilities.getPageLinks(currentUrl, body); 
  //we need the following because the Promise we create next 
  //will never settle if there are no tasks to process 
  if(links.length === 0) { 
    return Promise.resolve(); 
  } 
 
  return new Promise((resolve, reject) => { 
    let completed = 0; 
    let errored = false; 
    links.forEach(link => { 
      let task = () => { 
        return spider(link, nesting - 1) 
          .then(() => { 
            if(++completed === links.length) { 
              resolve(); 
            } 
          }) 
          .catch(() => { 
            if (!errored) { 
              errored = true; 
              reject(); 
            } 
          }); 
      }; 
      downloadQueue.pushTask(task); 
    });  
  }); 
} 

There are a couple of things in the previous code that deserve our attention:

  • First, we needed to return a new promise created using the Promise constructor. As we will see, this enables us to resolve the promise manually, when all of the tasks in the queue are completed.
  • Second, we should look at how we defined the task. What we did is attach an onFulfilled() callback to the promise returned by spider(), so we could count the number of completed downloaded tasks. When the amount of completed downloads matches the number of links in the current page, we know that we are done processing, so we can invoke the resolve() function of the outer promise.

Note

The Promises/A+ specification states that the onFulfilled() and onRejected() callbacks of the then() method have to be invoked only once and exclusively (only one or the other is invoked). A compliant promises implementation makes sure that even if we call resolve or reject multiple times, the promise is either fulfilled or rejected only once.

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

Exposing callbacks and promises in public APIs

As we have learned in the previous paragraphs, promises can be used as a nice replacement for callbacks. They turn out to be very useful for making our code more readable and easy to reason about. While promises bring many advantages, they also require the developer to understand many non-trivial concepts in order to be used correctly and proficiently. For this and other reasons, in some cases it might be more practical to prefer callbacks to promises.

Now let's imagine for a moment that we want to build a public library that performs asynchronous operations. What do we do? Do we create a callback-oriented API or a promise-oriented one? Do we need to be opinionated on one side or another or there are ways to support both and make everyone happy?

This is a problem that many well-known libraries face and there are at least two approaches that are worth mentioning that allow us to provide a versatile API.

The first approach, used by libraries such as request, redis, and mysql, consists of offering a simple API that is only based on callbacks and leave the developer the option to promisify the exposed functions if needed. Some of these libraries provide helpers to be able to promisify all the asynchronous functions they offer, but the developer still needs to somehow convert the exposed API to be able to use promises.

The second approach is more transparent. It also offers a callback-oriented API, but it makes the callback argument optional. Whenever the callback is passed as an argument, the function will behave normally, executing the callback on completion or on failure.When the callback is not passed, the function will immediately return a Promise object. This approach effectively combines callbacks and promises in a way that allows the developer to choose at call time what interface to adopt, without any need to promisify the function in advance. Many libraries, such as mongoose and sequelize, support this approach.

Let's see a simple implementation of this approach with an example. Let's assume we want to implement a dummy module that executes divisions asynchronously:

module.exports = function asyncDivision (dividend, divisor, cb) { 
  return new Promise((resolve, reject) => {               // [1] 
 
    process.nextTick(() => { 
      const result = dividend / divisor; 
      if (isNaN(result) || !Number.isFinite(result)) { 
        const error = new Error('Invalid operands'); 
        if (cb) { cb(error); }                            // [2] 
        return reject(error); 
      } 
 
      if (cb) { cb(null, result); }                       // [3] 
      resolve(result); 
    }); 
 
  }); 
}; 

The code of the module is very straightforward, but there are some details that are worth underlining:

  • First, were return a new promise created using the Promise constructor. We define the whole logic inside the function passed as argument to the constructor.
  • In the case of an error, we reject the promise, but if the callback was passed at call time, we also execute the callback to propagate the error.
  • After we calculate the result we resolve the promise, but again, if there's a callback, we propagate the result to the callback as well.

Let's see now how we can use this module with both callbacks and promises:

// callback oriented usage 
asyncDivision(10, 2, (error, result) => { 
  if (error) { 
    return console.error(error); 
  } 
  console.log(result); 
}); 
 
// promise oriented usage 
asyncDivision(22, 11) 
  .then(result => console.log(result)) 
  .catch(error => console.error(error)); 

It should be clear that with very little effort, the developers who are going to use our new module will be able to easily choose the style that best suits their needs, without having to introduce an external promisification function whenever they want to leverage promises.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.18.198