jpcasa
12/20/2018 - 7:13 PM

JS Promises

What is a Promise? A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred). A promise may be in one of 3 possible states: fulfilled, rejected, or pending. Promise users can attach callbacks to handle the fulfilled value or the reason for rejection.

Promises are eager, meaning that a promise will start doing whatever task you give it as soon as the promise constructor is invoked. If you need lazy, check out observables or tasks.

ES6 introduced the concept of job queue/micro-task queue which is used by Promises in JavaScript. The difference between the message queue and the job queue is that the job queue has a higher priority than the message queue, which means that promise jobs inside the job queue/ micro-task queue will be executed before the callbacks inside the message queue.

For example:

console.log('Script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

new Promise((resolve, reject) => {
    resolve('Promise resolved');
  }).then(res => console.log(res))
    .catch(err => console.log(err));

console.log('Script End');

Output:

Script start
Script End
Promise resolved
setTimeout

We can see that the promise is executed before the setTimeout, because promise response are stored inside the micro-task queue which has a higher priority than the message queue.

Let’s take another example, this time with two promises and two setTimeout. For example:

console.log('Script start');

setTimeout(() => {
  console.log('setTimeout 1');
}, 0);

setTimeout(() => {
  console.log('setTimeout 2');
}, 0);

new Promise((resolve, reject) => {
    resolve('Promise 1 resolved');
  }).then(res => console.log(res))
    .catch(err => console.log(err));

new Promise((resolve, reject) => {
    resolve('Promise 2 resolved');
  }).then(res => console.log(res))
    .catch(err => console.log(err));
    
console.log('Script End');

This prints:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
setTimeout 1
setTimeout 2

We can see that the two promises are executed before the callbacks in the setTimeout because the event loop prioritizes the tasks in micro-task queue over the tasks in message queue/task queue.

While the event loop is executing the tasks in the micro-task queue and in that time if another promise is resolved, it will be added to the end of the same micro-task queue, and it will be executed before the callbacks inside the message queue no matter for how much time the callback is waiting to be executed.

For example:

console.log('Script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

new Promise((resolve, reject) => {
    resolve('Promise 1 resolved');
  }).then(res => console.log(res));
  
new Promise((resolve, reject) => {
  resolve('Promise 2 resolved');
  }).then(res => {
       console.log(res);
       return new Promise((resolve, reject) => {
         resolve('Promise 3 resolved');
       })
     }).then(res => console.log(res));
     
console.log('Script End');

This prints:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
Promise 3 resolved
setTimeout

So all the tasks in micro-task queue will be executed before the tasks in message queue. That is, the event loop will first empty the micro-task queue before executing any callback in the message queue.

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'); // A

function getDataFromServer() {
  const result = axios.get('https://jsonplaceholder.typicode.com/posts/1'); // B
  return result; // C
}
  • On line A, we load the axios module which is a promise-based http client
  • On line B, we make a GET request to a public api endpoint and store the result in the result constant
  • On line C, 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 them.

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: the promise returned will include the result of the query

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.

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));

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, evaluates them in random order, 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, but not in the order that they were processed. Note that Promise.all does not process the promises in-order, it will evaluate them in the order that it prefers. In the next section we will look at executing promises in-order.

Promises In-order

If you want to run a couple of asynchronous tasks in order, 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 the order that we like. Please note that you must return a value in the then block, otherwise your sequence will not be executed in the order that you intended. That’s why you cannot simply call an async function in a then block and assume that it will happen in the right order. Below is an example of such code that you should always avoid:

getUserData()
  .then(info => {
    authenticate(info)
    .then(authResult => {
      doSomething(authResult);
    });
  });

Instead use proper chaining and make sure to return a value at each step:

getUserData()
.then(info => authenticate(info))
.then(authResult => doSomething(authResult))

In the snippet above note that since we are using arrow functions without a {} block, the right side of the => is implicitly returned.

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 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, as mentioned before, 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.

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.

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));