DEV Community

Cover image for Read files using Promises in Node.js
Amin
Amin

Posted on

Read files using Promises in Node.js

This article assumes that you have a slight understanding of what Node.js is and have used it at least once or twice to understand the inner concepts behind this article.

If you don't know yet, you can use Promises on some of the core modules of Node.js.

Let's see an example by reading one or more files and why you would want to use this API instead of the old Callback-style API.

Reading a file

Before we jump into that topic and explain why you might want to use the newer Promise-based API, let's see how we used to read a file using the old API.

$ touch index.js
Enter fullscreen mode Exit fullscreen mode
"use strict";

const {readFile} = require("fs");

readFile("package.json", (error, fileBuffer) => {
  if (error) {
    console.error(error.message);
    process.exit(1);
  }

  const fileContent = fileBuffer.toString();

  console.log(fileContent);
});
Enter fullscreen mode Exit fullscreen mode
$ node index.js
output of your package.json or an error here
Enter fullscreen mode Exit fullscreen mode

Pretty standard stuff here. We simply read our package.json file and handle any possible error by stopping the execution of our script.

What about reading multiple files then?

Reading files

Let's see how we can use the same callback-based API to read some more files in our scripts.

"use strict";

const {readFile} = require("fs");

const fileRead = (path) => {
  readFile(path, (error, fileBuffer) => {
    if (error) {
      console.error(error.message);
      process.exit(1);
    }

    const fileContent = fileBuffer.toString();

    console.log(fileContent);
  });
};

fileRead("package.json");
fileRead("README.md");
fileRead("index.js");
Enter fullscreen mode Exit fullscreen mode

Here nothing really strange, and again pretty basic stuff. We even used a function for reading multiple files.

But there is one major issue with this code: it is out of control.

If you try to read your files that way, you have no guarantee that one file will be read after another. You might say hey, this is the expected behavior because Node.js is an asynchronous platform and you will be absolutely right.

But if we want to have some more control and have our files read one after the other, we would have to use the same Callback-style API as readFile.

"use strict";

const {readFile} = require("fs");

const fileRead = (path, callback = null) => {
  readFile(path, (error, fileBuffer) => {
    if (error) {
      console.error(error.message);
      process.exit(1);
    }

    const fileContent = fileBuffer.toString();

    console.log(fileContent);

    if (callback) {
      callback();
    }
  });
};

fileRead("package.json", () => {
  fileRead("README.md", () => {
    fileRead("index.js");
  });
});
Enter fullscreen mode Exit fullscreen mode

Now our code is fixed! Our files are read in the order we expect them to be read.

But imagine now reading a hundred files. We would easily fall into what is called callback hell.

But fear not, because Promises are an answer to that problem!

Read a file (again)

This time, we will try to use the newer Promise-based API for reading a file.

"use strict";

const {promises: {readFile}} = require("fs");

readFile("index.js").then(fileBuffer => {
  console.log(fileBuffer.toString());
}).catch(error => {
  console.error(error.message);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Alright, there it is! We are doing exactly the same thing as earlier: reading a file. But we used a Promise-based API to do so.

One of the greatest advantages of that is that it scales. Oh man does it scale. You could be reading two or one hundred files and you could be using a syntax that is easily maintainable whatever the file count.

Reading files (again)

Let's see how we can rewrite reading multiple files but this time using our Promise-based API.

"use strict";

const {promises: {readFile}} = require("fs");

Promise.all([
  readFile("package.json"),
  readFile("README.md"),
  readFile("index.js")
]).then(([packageJson, readme, indexjs]) => {
  console.log(packageJson.toString());
  console.log(readme.toString());
  console.log(indexjs.toString());
}).catch(error => {
  console.error(error.message);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Here we used Promise.all to easily wait for all promises in our array to be resolved or rejected (when a file is missing).

This allows us to use a nice and clean API for when we want to read multiple files. And we don't have to use callbacks to handle each file.

We can even re-order the display of our files if we want to.

One might say that adding more and more files can make the Promise.then callback parameters grow in size. And that person would be totally right.

Let's see what we can do if we want to foresee a future where we would need to read a hundred files.

Reading hundreds of files (somehow)

"use strict";

const {promises: {readFile}} = require("fs");

const files = [
  "package.json",
  "README.md",
  "index.js"
];

Promise.all(files.map(file => {
  return readFile(file);
})).then(fileBuffers => {
  fileBuffers.forEach(fileBuffer => {
    console.log(fileBuffer.toString());
  });
}).catch(error => {
  console.error(error.message);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

That's it, really. What we did is use an array to store our files. We then mapped on each file and return an array of promises, just like earlier, and then we mapped over each one of our resolved file buffers to display them in the console.

This is all that is necessary to display one, two, a hundred or a thousand files to the console with the only need to add the needed files in the files array.

Bonus: GNU cat

Let's see what it takes to re-invent the wheel and create our own cat utility program.

For those of you that are unknown to what it does, it simply takes all of its arguments as a file and outputs their content.

Reminding you something we did earlier? Yep. That's almost what we did.

#!/usr/bin/env node

"use strict";

const {promises: {readFile}} = require("fs");

const files = process.argv.slice(2);

Promise.all(files.map(file => {
  return readFile(file);
})).then(fileBuffers => {
  fileBuffers.forEach(fileBuffer => {
    console.log(fileBuffer.toString());
  });
}).catch(error => {
  console.error(error.message);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

The only thing that changed is that it is now using process.argv instead of our manually crafted file array.

This means that every file that is passed as an argument can be used and will be used to read its content.

The shebang (first line) is there to help our shell because we will try to make it blend into our environment. Shhhhhhh.

$ mv index.js cat
$ chmod +x cat
$ ./cat README.md index.js package.json
[output truncated, but it worked!]
Enter fullscreen mode Exit fullscreen mode

13 single-lines of code for a cat-clone with error handling. Pretty cool, huh?

Conclusion

We saw what we used to use to read files using the old callback-based API in Node.js and the newer using a Promise-based API.

So if you are using Node.js and are stuck maintaining old API using a callback-style of doing things, know that you can upgrade things and get to the next step with this Promise-based API. readFile is only one of the many utilities that are available in the newer API of Node.js.

If I were to give an opinion on that, I think that the Promise-based API looks way cooler and more maintainable than its predecessor. And it allows us to leverage all the Promise.prototype methods that help us deal with asynchronous instructions easier.

If we go a little deeper in the analysis, reading files can be memory inefficient, especially when using the readFile method. If you really need to read more files that are also really heavy (like encrypting videos) you should be using createReadStream instead.

But then you will not be using Promises anymore but Streams. And that's a whole new topic that I won't cover here (but maybe another day).

Top comments (0)