Working with Multiple Promises

In the examples so far, promises were used to implement a single asynchronous task. In nontrivial scenarios, we may need multiple asynchronous tasks to be executed to solve a problem. For example, a currency exchange broker may want to pick the best price quote from more than one provider. A promise carries the response for a single asynchronous task, but when dealing with multiple tasks, we need to combine data from multiple asynchronous functions, each returning a promise.

JavaScript provides two options to deal with multiple asynchronous tasks:

  • Let the tasks race and pick the first promise that resolves or rejects.
  • Wait for all the tasks to resolve or for any one of them to reject.

Let’s explore each of these options with examples.

Racing Promises

The race() static method of Promise takes an array of promises and returns the first one to resolve or reject. Let’s create two promises, one that resolves after a delay and the other that rejects after a timeout has expired.

 const​ createPromise = ​function​(timeInMillis) {
 return​ ​new​ Promise(​function​(resolve, reject) {
  setTimeout(() => resolve(timeInMillis), timeInMillis);
  });
 };
 
 const​ createTimeout = ​function​(timeInMillis) {
 return​ ​new​ Promise(​function​(resolve, reject) {
  setTimeout(() => reject(​`timeout after ​${timeInMillis}​ MS`​), timeInMillis);
  });
 };

The createPromise() function returns a promise that resolves after the time given as a parameter. The createTimeout() function returns a promise that rejects after the time given as a parameter. Let’s create a few promises using these functions and let them compete with each other.

 Promise.race([createPromise(1000), createPromise(2000), createTimeout(3000)])
  .then(result => console.log(​`completed after ​${result}​ MS`​))
  .​catch​(error => console.log(​`ERROR: ​${error}​`​));
 
 Promise.race([createPromise(3500), createPromise(4000), createTimeout(2000)])
  .then(result => console.log(​`completed after ​${result}​ MS`​))
  .​catch​(error => console.log(​`ERROR: ​${error}​`​));

To the first call to race(), we pass three promises: one that will finish in one second, the second that will finish in two seconds, and the third that will time out after three seconds. Since the first will finish first, the result reported within then() will be the result of this promise.

To the second call to race() we pass three promises as well, but in this case the timeout will occur before the other two promises complete. The catch() function will be used in this case instead of the then() function.

Let’s confirm this behavior from the output:

 completed after 1000 MS
 ERROR: timeout after 2000 MS

There are times when we may want to pick among multiple solutions to a given problem. For example, in optimization problems, there may be multiple optimal solutions and any one of them may be acceptable. In these cases we don’t have to wait for the completion of all solutions—the first one to finish is adequate. Use the race() function to pick one among multiple asynchronous tasks that return promises.

Gathering All Promises

The all() static method of promise takes an array of promises and passes an array of resolved results to the then() function when all promises resolve. If any one of the given promises is rejected, then the then() function is not called; the catch() function is used instead.

We will soon use the all() function to work with multiple asynchronous tasks. But before we get to the code that uses the all() function, we need to create a small program that will execute calls concurrently. This will help us to see the power of the all() method when run asynchronously and concurrently.

 'use strict'​;
 
 const​ cluster = require(​'cluster'​);
 const​ http = require(​'http'​);
 const​ url = require(​'url'​);
 const​ querystring = require(​'querystring'​);
 const​ port = 8084;
 const​ number_of_processes = 8;
 
 const​ isPrime = ​function​(number) {
 for​(​let​ i = 2; i < number; i++) {
 if​ (number % i === 0) {
 return​ ​false​;
  }
  }
 
 return​ number > 1;
 };
 
 const​ countNumberOfPrimes = ​function​(number) {
 let​ count = 0;
 
 for​(​let​ i = 1; i <= number; i++) {
 if​(isPrime(i)) {
  count++;
  }
  }
 
 return​ count;
 };
 
 const​ handler = ​function​(request, response) {
 const​ params = querystring.parse(url.parse(request.url).query);
 
 const​ number = parseInt(params.number);
 
 const​ count = countNumberOfPrimes(number);
 
  response.writeHead(200, { ​'Content-Type'​: ​'text/plain'​ });
 return​ response.end(​`​${count}​`​);
 };
 
 if​(cluster.isMaster) {
 for​(​let​ i = 0; i < number_of_processes; i++) {
  cluster.fork();
  }
 } ​else​ {
  http.createServer(handler).listen(port);
 }

This example shows a small HTTP server that runs in node.js. The isPrime() function returns a boolean result of true or false depending on whether or not the given number is a prime number. It has been written intentionally to be slow or time consuming. The countNumberOfPrimes() function returns the number of prime numbers between 1 and the given number. The handler() function parses the query string from an HTTP request, extracts the number parameter, computes the number of primes in the range 1 to that number, and sends the result via the HTTP response. The code, when started, runs a cluster of eight processes that can process the incoming requests concurrently.

Let’s now work on the code that will use this service. First, we need to install both the fs-extra and the request-promise packages using the following command:

 npm install fs-extra request request-promise

We included request in that list of packages since request-promise needs it.

Now, let’s write the code to use the service.

 const​ fs = require(​'fs-extra'​);
 const​ request = require(​'request-promise'​);
 
 const​ countPrimes = ​function​(number) {
 if​(isNaN(number)) {
 return​ Promise.reject(​`'​${number}​' is not a number`​);
  }
 
 return​ request(​`http://localhost:8084?number=​${number}​`​)
  .then(count => ​`Number of primes from 1 to ​${number}​ is ​${count}​`​);
 };

The countPrimes() method receives a number and returns a promise. If the given parameter is not a number, it immediately returns a promise in the rejected state. On the other hand, if the given parameter is a number, then it makes an asynchronous call to the service that computes the number of primes.

Suppose we have a file where each line is a number and we want to determine the number of primes for the numbers on each line. We can compute the values for each line at the same time, asynchronously. Using the readFile() methods of fs-extra, we may asynchronously process the contents of an input file. Then, for each line we can invoke the countPrimes() method. We can use the map() method of the array to transform the array of lines to an array of promises. Then, using the all() method we can wait for the completion of the asynchronous calls. The countPrimesForEachLine() function takes the path to a file and prints the details of the count.

 const​ countPrimesForEachLine = ​function​(pathToFile) {
  fs.readFile(pathToFile)
  .then(content => content.toString())
  .then(content =>content.split(​'​​​​n'​))
  .then(lines => Promise.all(lines.map(countPrimes)))
  .then(counts => console.log(counts))
  .​catch​(error => console.log(error));
 };

The call to the catch() function will kick in if there was a failure in processing the file or any of the lines in the file.

Let’s create two files. The first file, numbers.txt, has lines with valid numbers:

 100
 1000
 5000

The second file, numbers-with-error.txt, has a line of text that is not a number:

 100
 invalid text
 5000

Let’s invoke countPrimesForEachLine(), passing the names of these two files.

 countPrimesForEachLine(​'numbers.txt'​);
 countPrimesForEachLine(​'numbers-with-error.txt'​);

For the file with valid numbers in it, the result will be the number of primes for each line. However, when the second file with invalid input is passed, the output shows the processing error—all() fails fast when any of the promises provided to it fail.

 'invalid text' is not a number
 [ 'Number of primes from 1 to 100 is 25',
  'Number of primes from 1 to 1000 is 168',
  'Number of primes from 1 to 5000 is 669' ]

One caveat with all() is that if computations were to take an indefinitely long amount of time, then it might lead to starvation. To avoid this concern, we can use a timeout promise, like the one we created earlier using the createTimeout() function. We can then combine the race() function and the all() function to achieve timeout if the task takes too long. We will explore this approach as part of the exercises at the end of this chapter.

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

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