NodeJS callback hell and Promise optimization

There are many asynchronous APIs in NodeJS, such as the common readFile method of the fs module. Although there is a synchronous version of readFileSync, its performance is definitely not as good as the former. So let’s start with the asynchronous version of readFile:

const fs = require('fs');

fs.readFile('./a.txt', 'utf-8', function(error, data) {
    if (!error) {
        console.log('a.txt data:', data);
    }
});

The function itself is relatively simple. The three parameters are file path, data encoding and callback function.

Now there is a need to read three txt files abc respectively (the file contents are 1, 2, 3 respectively, the file path is the same as the js file path), and sequentially output the contents of the files, also The output is 1 2 3. If read in parallel, it looks like this:

const fs = require('fs');

fs.readFile('./a.txt', 'utf-8', function(error, data) {
    if (!error) {
        console.log('a.txt data:', data);
    }
});

fs.readFile('./b.txt', 'utf-8', function(error, data) {
    if (!error) {
        console.log('b.txt data:', data);
    }
});

fs.readFile('./c.txt', 'utf-8', function(error, data) {
    if (!error) {
        console.log('c.txt data:', data);
    }
});

If you run it multiple times, you will find that the order of the output is inconsistent each time:

Of course, there are more than just these three. If you are interested, you can run it a few more times to see.

So if you want to have a fixed output of 1 2 3, you have to write it like this. After reading a.txt, you can read b, and after reading b, you can read c:

const fs = require("fs");

fs.readFile("./a.txt", "utf-8", function (error, data) {
  if (!error) {
    console.log("a.txt data:", data);
    fs.readFile("./b.txt", "utf-8", function (error, data) {
      if (!error) {
        console.log("b.txt data:", data);
        fs.readFile("./c.txt", "utf-8", function (error, data) {
          if (!error) {
            console.log("c.txt data:", data);
          }
        });
      }
    });
  }
});

Although the output is in order, the code is nested. If there are more files and the processing logic after reading is more complex, the readability of the entire code will become very poor. This iscallback hell. To solve this method, we introduced Promise.

Promise can be simply understood as a container that wraps an asynchronous function. A basic example:

const fs = require("fs");

const readAPromise = new Promise(function (resolve, reject) {
  fs.readFile("./a.txt", "utf-8", function (error, data) {
    if (error) {
        reject(error);
    } else {
        resolve(data);
    }
  });
});

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

First try chaining calls to transform the callback hell code:

const fs = require("fs");

function readFile(filePath, defaultCoding = "utf-8") {
  return new Promise(function (resolve, reject) {
    fs.readFile(filePath, defaultCoding, function (error, data) {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

readFile("./a.txt")
  .then(function (data) {
    console.log(data);
    return readFile("./b.txt");
  })
  .then(function (data) {
    console.log(data);
    return readFile("./c.txt");
  })
  .then(function (data) {
    console.log(data);
  })
  .catch(function (error) {
    console.error(error);
  });

Then try using Promise.all:

const fs = require("fs");

function readFile(filePath, defaultCoding = "utf-8") {
  return new Promise(function (resolve, reject) {
    fs.readFile(filePath, defaultCoding, function (error, data) {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

Promise.all([readFile("./a.txt"), readFile("./b.txt"), readFile("./c.txt")])
  .then(function (data) {
    console.log(data);
  })
  .catch(function (error) {
    console.error(error);
  });

This encapsulates a Promise function that returns the file reading result. Then call Promise.all, the first parameter is an array of Promise objects. If all are successful, it will go to the data in .then. Data is the result array of each promise resolve. Here it prints [‘1’, ‘2’, ‘3’ ]; if one fails, The entire Promise array will go to .catch. To solve this problem, you can try Promise.allSettled

const fs = require("fs");

function readFile(filePath, defaultCoding = "utf-8") {
  return new Promise(function (resolve, reject) {
    fs.readFile(filePath, defaultCoding, function (error, data) {
        if (filePath === './b.txt') {
            reject('cannot read b file');
        }
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

Promise.allSettled([readFile("./a.txt"), readFile("./b.txt"), readFile("./c.txt")])
  .then(function (data) {
    console.log(data);
  })
  .catch(function (error) {
    console.error(error);
  });

result:

[
  { status: 'fulfilled', value: '1' },
  { status: 'rejected', reason: 'cannot read b file' },
  { status: 'fulfilled', value: '3' }
]

Also add, use async and await for further optimization:

Basic usage of asynchronous functions:

async function f() {
  return "a";
}

// console.log(f()); // Promise { 'a' }
f()
  .then(function (data) {
    console.log(data); // a
  })
  .catch(function (error) {
    console.error(error);
  });

Complete example

const fs = require("fs");

function readFile(filePath, defaultCoding = "utf-8") {
  return new Promise(function (resolve, reject) {
    fs.readFile(filePath, defaultCoding, function (error, data) {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

async function readAllFile() {
  const aFile = await readFile("./a.txt");
  const bFile = await readFile("./b.txt");
  const cFile = await readFile("./c.txt");
  return [aFile, bFile, cFile];
}

readAllFile().then(([a, b, c]) => console.log(a, b, c));

Using the promisify method of the util module for further optimization, you can remove the packaging fs.readFile and return the Promise function, directly returning the Promise object without writing a callback, example:

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

async function readAllFile() {
  const aFile = await readFile("./a.txt", "utf-8");
  const bFile = await readFile("./b.txt", "utf-8");
  const cFile = await readFile("./c.txt", "utf-8");
  return [aFile, bFile, cFile];
}

readAllFile().then(([a, b, c]) => console.log(a, b, c));