web #node.js #javascript #promises

Promisifying the 'fs' Module to use Async/Await

Jan 4 '19 · 5 min read · 996

Returning from callback hell (and the thenable abyss). Learn how to make modules that use callbacks async/await ready.

Prerequisite

T

his post will assume you know all about callbacks, callback hell, what Promises are, and why they give you hope in avoiding a sit down with Satan. If you don't how to use Promises or Async/Await, take a look at the MDN documentation - or your source of choice - before reading this post.

As a reminder (straight from MDN), a Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason. This is a bit cryptic if you don't know what a Promise is - so go read that documentation!

Node's Built-in 'fs' Module

The built-in 'fs' module allows you to access and control the file system of the device that your Node application is running on (e.g. the file system of an EC2 or Heroku instance). This means creating, writing, reading, deleting, moving files from your application is dead easy. The most obvious and perhaps biggest applications for this (to me atleast) are cloud storage services, and sites that allow photo uploading.

So, all in all, file system manipulation is incredibly important for web applications and Node.js makes it really easy to do.

A typical call to the 'fs' module looks like this...

const fs = require('fs');

fs.readFile('/some/path/to/file.txt', (err, data) => {
  if (err) 
    return console.log(err);
  console.log(data);
});

The code above shows the ease with which you can manipulate the file system, but the lack of ES6/ES7+ syntax also reveals the ease with which you can quickly end up with a mess of callbacks if you application is heavily dependent upon file access and storage.

"If you need more than 3 levels of indentation, you're screwed anyway, and should fix your program."
- Linus Torvalds

For example, when you need to apply a succession of photo manipulations and transformations before saving or sending to a client. This is a prime example where Promises shine: each successive photo manipulation would just tack on extra 'then' block, avoiding additional levels of indentation. Moreover, you now get the benefits of being able to use Async/Await; more succinct, readable and maintainable code. So let's take advantage of that fancy ES6/ES7+ syntax!

Promisifying

Promisifying is the simple process of creating a wrapper function around a function that takes a callback as an argument, where the wrapper returns a Promise. For the 'fs' module (or any other module that needs to be Promisified), I create a utility JavaScript file where I Promisify the 'fs' functions that I need to use in my code and export them using module.exports. I then just need to require this JavaScript file - instead of the 'fs' module - to use the Promisified functions. In fact, the first code snippet below is of code used for this very blog.

The Utility Script: 'fs-promisify.js'

const fs = require('fs');

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

const appendFile = (path, data) => {
  return new Promise((resolve, reject) => {
    fs.appendFile(path, data + "\n", (err) => {
      if (err)
        reject(err);
      resolve(data);
    });
  });
};

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

module.exports = {
  readFile,
  appendFile,
  writeFile
};

Using 'fs-promisify.js'

const fsPromisify = require('./utils/fs-promisify');

// Using with 'then' blocks
(() => {
  fsPromisify.readFile('/path/to/file.txt')
  .then(data => console.log(data))
  .catch(err => console.log(err));
})();

// Using with async/await
(async () => {
  try {
    const data = await fsPromisify.readFile('/path/to/file.txt');
    console.log(data);
  } catch (e) {
    console.log(e);
  }
})();