DEV Community

Cover image for Simple code with fs.promises and async await
PuruVJ
PuruVJ

Posted on • Originally published at puruvj.dev

Simple code with fs.promises and async await

Read in dark or mid-day mode on my blog

Hi! I see you have jumped onto my blog. Well, buckle up, this is gonna be one helluva ride!! We're gonna explore how to use the all-time favorite async / await feature with Node's Filesystem API.

So now, let's make a super-simple program to read the username and password from a file, encrypt the password(Always do it, kids 😉), and write the username and new password to some other file.

So let's write up in plain english how our code works

1. Read the `user-data.json` file.
2. Throw error if any.
3. Extract `username`, `password` from the file contents.
4. Encrypt the password.
5. Assemble final data to be written into the new file.
6. Write the data to the `user-data-final.json` file
7. Throw error if any.
8. Output if successful
Enter fullscreen mode Exit fullscreen mode

Seems straightforward enough. So let's write it out in actual code.

const fs = require('fs');

function main() {
  fs.readFile('user-data.json', (err, data) => {
    if (err) throw err;

    // Let's process the data
    const { username, password } = JSON.parse(data);

    // Let's encrypt
    const encryptedPassword = encrypt(password);

    const finalObject = { username, password: encryptedPassword };

    // Let's write it to another file
    fs.writeFile('user-data-final.json', JSON.stringify(finalObject), (err) => {
      if (err) throw err;

      console.log('Successful');
    });
  });
}

try {
  main();
} catch (e) {
  console.error(e);
}
Enter fullscreen mode Exit fullscreen mode

We're just catching the errors and throwing them out to the console, in the last try-catch block.

This seems to work.

But something nags me here. Look at the steps I wrote out in plain english, and then look at the code. Plain english steps look very sequential, and step by step. Whereas the code we wrote, it is sequential, but it feels like all the steps live inside step 1, and step 7 and 8 live inside step 6. In short:

1.
  2.
  3.
  4.
  5.
  6.
    7.
    8.
Enter fullscreen mode Exit fullscreen mode

Doesn't feel so idiomatic anymore, does it? It feels weird that all these steps in the code have to live inside of other steps, whereas in what we wrote, it feels idiomatic, like passing the torch in olympics(or in whatever events the torch is passed, I ain't a sports junkie 😁).

How can I make the code idiomatic, and mirror the steps it's based on?

Solution(s)

Well, callback pattern can be replaced by using async / await. We can flatten our code a lot using them. But await works only with promises, ie.

const result = await fetch('https://api.example.com');
Enter fullscreen mode Exit fullscreen mode

fetch here returns a promise, so we can await the result. How do we promisify our writeFile and readFile methods then 🤔?

Well, look at this code below:

const readFile = (path) =>
  new Promise((resolve, reject) =>
    fs.readFile(path, (err, data) => {
      if (err) reject(err);

      resolve(data);
    })
  );
Enter fullscreen mode Exit fullscreen mode

This is a promise based implementation of the readFile function. We can use it as simply as this 👇

const data = await readFile('user-data.json');
Enter fullscreen mode Exit fullscreen mode

This will read the file, and move on to the next line after the data has come through. No indentation, no branching, nothing, Nada!! It looks good. So let's implement our complete code with this method.

const fs = require('fs');

const readFile = (path) =>
  new Promise((resolve, reject) =>
    fs.readFile(path, (err, data) => {
      if (err) reject(err);

      resolve(data);
    })
  );

const writeFile = (path, data) =>
  new Promise((resolve, reject) =>
    fs.writeFile(path, data, (err) => {
      if (err) reject(err);

      resolve();
    })
  );

async function main() {
  const data = await readFile('user-data.json');

  // Extract
  const { username, password } = JSON.parse(data);

  // Let's encrypt
  const encryptedPassword = encrypt(password);

  const finalObject = { username, password: encryptedPassword };

  // Let's write to another file
  await writeFile('user-data-final.json', JSON.stringify(finalObject));

  console.log('Successful');
}

try {
  main();
} catch (e) {
  console.error(e);
}
Enter fullscreen mode Exit fullscreen mode

Look at our main function here. The overall code is bigger, but our main function, which is the actual logic, is much more simpler and actually follows the steps we wrote, in the idiomatic way we imagined.

Simpler way (utils.promisify)...

Our code above looks quite big, due to defining the promise-based versions of writeFile and readFile. We can make it much, much smaller by using a utility function exported by Node itself, promisify.

Usage 👇

const { promisify } = require('util');
const fs = require('fs');

const writeFile = promisify(fs.writeFile);
Enter fullscreen mode Exit fullscreen mode

You simply pass the callback-based function to the promisify function, and voila! you have a promise-based version of your original function.

So our code now becomes 👇

const { promisify } = require('util');
const fs = require('fs');

const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);

async function main() {
  const data = await readFile('user-data.json');

  // Extract
  const { username, password } = JSON.parse(data);

  // Let's encrypt
  const encryptedPassword = encrypt(password);

  const finalObject = { username, password: encryptedPassword };

  // Let's write to another file
  await writeFile('user-data-final.json', JSON.stringify(finalObject));

  console.log('Successful');
}

try {
  main();
} catch (e) {
  console.error(e);
}
Enter fullscreen mode Exit fullscreen mode

So much smaller 😍.

...Simplest Way!

Now lemme introduce you to the Ace in the sleeve! Since version 10, NodeJS exports promise based versions of its methods, by default. They can be accessed by require('fs').promises.

Here's our final code using this approach:

const { writeFile, readFile } = require('fs').promises;

async function main() {
  const data = await readFile('user-data.json');

  // Extract
  const { username, password } = JSON.parse(data);

  // Let's encrypt
  const encryptedPassword = encrypt(password);

  const finalObject = { username, password: encryptedPassword };

  // Let's write to another file
  await writeFile('user-data-final.json', JSON.stringify(finalObject));

  console.log('Successful');
}

try {
  main();
} catch (e) {
  console.error(e);
}
Enter fullscreen mode Exit fullscreen mode

Notice the first line. We're directly importing the writeFile and readFile methods from require(fs).promises. This is the best and the cleanest version you can find in Node currently.

Code Conventions

Now that you've seen how to use fs.promises, let's find out the best patterns to use this code.

Importing individual functions

const { writeFile, readFile, access } = require('fs').promises;
Enter fullscreen mode Exit fullscreen mode

This is probably the most convenient method, and the cleanest too. But the problem arises when you have to import something from regular fs module. For example 👇

const { writeFile, readFile, access } = require('fs').promises;
const { writeFileSync, createReadStream, createWriteStream } = require('fs');
Enter fullscreen mode Exit fullscreen mode

We are importing the promise based functions, as well as some functions from regular fs, like streams. Now you can directly use it down in your main logic, but sometimes when the code in the file gets big enough, and I'm not exactly using await with the promise-based versions, it can get pretty confusing which method is coming from where, so I have to scroll all the way to the top to see the imports.

This may not seem like a big problem, but I challenge you to write this code and comeback to it after 6 months. You'll be in the same dilemma 😂

Importing as namespace

This is my most preferred method.

const fs = require('fs');
const fsp = fs.promises; // 👈 This line

...

await fsp.writeFile();

fs.createReadStream();
Enter fullscreen mode Exit fullscreen mode

ES Imports

Now that we can use ES Imports in Node(with some extra tweaking), let's consider the Modular version

import { promises as fsp } from 'fs';

async function main() {
  const data = await fsp.readFile('user-data.json');

  // Extract
  const { username, password } = JSON.parse(data);

  // Let's encrypt
  const encryptedPassword = encrypt(password);

  const finalObject = { username, password: encryptedPassword };

  // Let's write to another file
  await fsp.writeFile('user-data-final.json', JSON.stringify(finalObject));

  console.log('Successful');
}

try {
  main();
} catch (e) {
  console.error(e);
}
Enter fullscreen mode Exit fullscreen mode

Also, if your node version is more than v14.8.0, you can also directly use top level await (I have an article about it, right here).

import { promises as fsp } from 'fs';

try {
  const data = await fsp.readFile('user-data.json');

  // Extract
  const { username, password } = JSON.parse(data);

  // Let's encrypt
  const encryptedPassword = encrypt(password);

  const finalObject = { username, password: encryptedPassword };

  // Let's write to another file
  await fsp.writeFile('user-data-final.json', JSON.stringify(finalObject));

  console.log('Successful');
} catch (e) {
  console.error(e);
}
Enter fullscreen mode Exit fullscreen mode

Even smaller!!!

Conclusion

Hope you got some good insights from this blog post.

Top comments (0)