Promises

A promise is an object that represents the result of an asynchronous operation that may or may not succeed when executed at some point in the future. For example, when you make a request to an API server, you can return a promise that would represent the result of the api call. The api call may or may not succeed, but eventually you will get a promise object that you can use. The function below performs an api call and returns the result in the form of a promise:

code/promises/axios-example.js

const axios = require('axios');
function getDataFromServer() {
  const result =  axios.get('https://jsonplaceholder.typicode.com/posts/1');
  return result;
}
  • On line 1, we load the axios module which is a promise-based http client
  • On line 3, we make a GET request to a public api endpoint and store the result in the result constant
  • On line 4, we return the promise

Now, we can simply call the function and access the results and catch possible errors:

getDataFromServer()
  .then(function(response) {
    console.log(response);
  })
  .catch(function(error) {
    console.log(error);
  });

Every promise has a then and a catch method. You would use the then method to capture the result of the operation if it succeeds (resolved promise), and the catch method if the operation fails (rejected promise). Note that both then and catch receive a callback function with a single argument to capture the result. Also, it's worth noting that both of these methods return a promise that allows us to potentially chain more promises.

Below are a couple of other examples of asynchronous tasks that can return a promise:

  • Reading the content of a file: the promise returned will include the content of the file
  • Listing the content of a directory: the promise returned will include the list of files
  • Parsing a csv file: the promise returned will include the parsed content
  • Running some query against a database to get some result

The figure below summaries the states that a promise can have:

Promise Advantages

Promises existed in other languages and were introduced to JavaScript to provide an abstraction over the callback mechanism. Callbacks are the primary mechanisms for dealing with asynchronous tasks, but they can get tedious to work with. Promises were implemented in JavaScript to simplify working with callbacks and asynchronous tasks. There are many articles out there about callback hassles, just google "callback hell in JavaScript" and the results won't disappoint you.

Making a Promise

We can create a promise using the global Promise constructor:

const myPromise = new Promise();

The promise constructor takes a callback function with two arguments. The first argument is used to resolve or capture the result of an asynchronous operation, and the second is used to capture errors:

const myPromise = new Promise(function(resolve, reject) {
  if(someError) {
    reject(new Error(someError));
  } else {
    resolve('ok');
  }
});

And as mentioned before, we can use the then method to use the results when the promise is resolved, and the catch method to handle errors:

myPromise
  .then(function(result) {
    console.log(result);
  })
  .catch(function(error) {
    console.log(error);
  });

It's worth mentioning that we can wrap any asynchronous operation in a promise. For example, the fs.readFile is an method that reads the content of a file asynchronously. The fs.readFile method is used as follows:

fs.readFile('some-file.txt', 'utf-8', function(error, content) {
  if(error) {
    return console.log(error);
  }
  console.log(content);
});

We can create a function called readFile that uses fs.readFile, reads the content of a file and resolves a promise with the content, or reject it if there is an error:

code/promises/wrap-readfile1.js

const fs = require('fs');
function readFile(file, format) {
  format = format || 'utf-8';
  function handler(resolve, reject) {
    fs.readFile(file, format, function(err, content) {
      if(err) {
        return reject(err);
      }
      return resolve(content);
    });
  }
  const promise = new Promise(handler);
  return promise;
}

The same code can be re written more concisely as follows:

code/promises/wrap-readfile2.js

const fs = require('fs');
function readFile(file, format = 'utf-8') {
  return new Promise((resolve, reject) => {
    fs.readFile(file, format, (err, content) => {
      if(err) return reject(err);
      resolve(content);
    });
  });
}

Now we can simply call our function and capture the result in the then method, and catch errors using the catch method:

readFile('./example.txt')
  .then(content => console.log(content))
  .catch(err => console.log(err));

Even more concisely we can re write the code above using the util.promisify method that was introduced in Node 8:

code/promises/promisify-example.js

const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);

readFile('./example.txt', 'utf-8')
  .then(content => console.log(content))
  .catch(err => console.log(err));

The util.promisify method takes a function that follows the Node callback convention and returns a promise-based version. You may be wondering why doesn't node make all the methods promise-based. Low level Node methods are not promise based because promises are higher level abstractions over callbacks. It's up to the programmer to decide whether or not they need a higher abstraction like promises to handle async operations.

Promise Static Methods

The Promise constructor has a couple of useful static methods that is worth exploring. All the code snippets are in code/promises/static-methods.js. Some notable ones are listed below:

Promise.resolve: a shortcut for creating a promise object resolved with a given value

function getData() {
  return Promise.resolve('some data');
}
getData()
  .then(d => console.log(d));

Promise.reject: a shortcut for creating a promise object rejected with a given value

function rejectPromise() {
  return Promise.reject(new Error('something went wrong'));
}
rejectPromise()
  .catch(e => console.log(e));

Promise.all: used to wait for a couple of promises to be resolved

const p1 = Promise.resolve('v1');
const p2 = Promise.resolve('v2');
const p3 = Promise.resolve('v3');

const all = Promise.all([p1, p2, p3]);

all.then(values => console.log(values[0], values[1], values[2]));

Note that Promise.all takes an array of promise objects and "waits" until all of them are resolved. Eventually it will return a promise object that contains all the values in an array in the order that they were submitted.

Promises in a Sequence

If you want to run a couple of asynchronous tasks in a sequence, you can follow the following pattern:

const promiseChain = task1()
  .then(function(task1Result) {
    return task2();
  })
  .then(function(task2Result) {
    return task3();
  })
  .then(function(task3Result){
    return task4();
  })
  .then(function(task4Result) {
    console.log('done', task4Result);
  })
  .catch(function(err) {
    console.log('Error', err);
  });

The promise chain is kicked off by calling the first task that returns a promise. Afterwards, the then method is called which also returns a promise allowing us to keep chaining the then calls. Let's look at an example to say how you may want to use this pattern.

Let's say we have a text file that contains a bunch of invalid characters the we need to remove. In order to accomplish that, first, we need to read the content of the file. Then, we need to remove the invalid characters, and finally write the results to another file. Assuming that we have a function for each operation that returns a promise, we can define the following promise chain:

code/promise/promise-in-sequence.js

const promiseChain = readFile('example.txt')
  .then(function(content) {
    return removeInvalidChracters(content);
  })
  .then(function(cleanContent) {
    return writeToFile('./clean-file.txt', cleanContent);
  })
  .then(function() {
    console.log('done');
  })
  .catch(function(error) {
    console.log(error);
  });

Using the above promise chain, each task is finished before the next one starts causing the tasks to happen "in order".

Running Promises Concurrently

When you call an asynchronous function that returns a promise, you can assume that the operation is executed asynchronously. Therefore, if you call each function one by one on each line, you are practically running each task concurrently:

function runAll() {
  const p1 = taskA();
  const p2 = taskB();
  const p3 = taskC();
}

runAll();

Now, if you want to do something when all these operations are finished, you can use Promise.all:

code/promises/run-all.js

function runAll() {
  const p1 = taskA();
  const p2 = taskB();
  const p3 = taskC();
  return Promise.all([p1, p2, p3]);
}

runAll()
  .then(d => console.log(d, 'all done'))
  .catch(e => console.log(e));

In the next section we will explore how you can combine promises that run concurrently and promises that need to run in-order.

Combining Promises

The main motivation for this section is mainly for the type of tasks that need to run concurrently and in sequence. Let's say you have a bunch of files that you need to manipulate asynchronously. You may need to perform operation A, B, C, D in order on 3 different files, but you don't care about the order that the files are processed in. All you care about is that the operations A, B, C, and D happen in the right order. We can use the following pattern to achieve that:

  1. Create a list of promises
  2. Each promise represents the sequence of async tasks A, B, C, D
  3. Use Promise.all to process all the promises. Note that the all method processes the promises concurrently
const files = ['a.txt', 'b.txt', 'c.txt'];

function performInOrder(file) {
  const promise = taskA(file)
  .then(taskB)
  .then(taskC)
  .then(taskD);
  return promise;
}

const operations = files.map(performInOrder);
const result = Promise.all(operations);

result.then(d => console.log(d)).catch(e => console.log(e));

Below is an actual code that you can run, assuming that you have the three files a.txt, b.txt and c.txt:

code/promises/read-write-multiple-files/main.js

const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const copyFile = (file) => (content) => (writeFile(file + '-copy.txt', content));
const replaceContent = input => (Promise.resolve(input.replace(/-/g, 'zzzz')));
const processEachInOrder = file => {
  return readFile(file, 'utf-8')
    .then(replaceContent)
    .then(copyFile(file));
}

const files = ['./a.txt', './b.txt', './c.txt'];
const promises = files.map(processEachInOrder);
Promise.all(promises)
  .then(d => console.log(d))
  .catch(e => console.log(e));

It's worth noting that this kind of processing can introduce a big workload on the CPU if the input size is large. A better approach would be to limit the number of tasks that are processed concurrently. The async library has a qeueue method that limits the number of async tasks that are processed at a time, reducing extra workload on the CPU. We will explore the async library in later sections.

Exercise

As an exercise, write a script that reads the content of a directory (1 level deep) and copys only the files to another directory called output.

Hint You can use the example from the previous section as a starting point

Hint Here is the general idea: read the content of the folder, use the stat method to figure out which entry is a file, make the output folder, read each file, and write to the output folder

Solution

Below is one possible solution that uses the Promise.all pattern to process the read-write promises:

code/promises/exercise/main.js

/*
  List the content of the folder, filter out the files only
  then copy to the output folder.
 */
const fs = require('fs');
const path = require('path');
const util = require('util');
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const readdir = util.promisify(fs.readdir);
const stat = util.promisify(fs.stat);
const mkdir = util.promisify(fs.mkdir);
const outputFolder = './output';

function isFile(f) {
  return stat(f).then(d => d.isFile() ? f : '');
}

function filterFiles(list) {
  return Promise.all(list.map(isFile))
    .then(files => files.filter(v => v));
}

function readWrite(result) {
  const files = result[1];
  return Promise.all(files.map(f => {
    return readFile(f)
    .then(content => writeFile(path.join(outputFolder, f), content));
  }));
}

const getFiles = readdir('./').then(filterFiles);

Promise.all([mkdir(outputFolder), getFiles])
  .then(readWrite)
  .then(_ => console.log('done!'))
  .catch(e => console.log(e));

results matching ""

    No results matching ""