Using Promisify to Avoid Javascript Callback Spaghetti

August 5, 2019

Asynchronous codes are common in JS programming, like fetching data from an endpoint and reading dir/files. Often they require us to pass a callback function that will be executed when the action is completed.

The problem with callback async

The problem with callback async is that they can get messy.

If I want to read a file (using fs.readFile), I can do it like this:

fs.readFile('./file/location.md', 'utf-8', function(err, val){
  if(err) throw new Error ("Something terrible happened")
  console.log("Content: ", val)
})
console.log("Waiting...")

You'll notice "Waiting" is displayed before "Content". This is because JS automatically moves all async functions at the back of the line (regardless how "fast" they execute).

Now this is a big deal if we need to use the result of that async function for our next action. If we need to use the result of our callback function, the following won't work:

let pathToNextLocation; 
fs.readFile('./file/location1.md', 'utf-8', function(err, val){
  if(err) throw new Error
  pathToNextLocation = val; 
})

console.log(pathToNextLocation); 

We will need to do this instead:

let pathToNextLocation
fs.readFile('./file/location1.md', 'utf-8', function(err, val){
  if(err) throw new Error
  pathToNextLocation = val; 
  fs.readFile(pathToNextLocation, 'utf-8', function(err, val) {
    // do stuff!
  })
})

What if we need to execute four async functions in sequence? We would have to nest it four levels deep. This is one big spaghetti.

big spaghetti

Better way to handle async: Promises

A better way to deal with async function is to use promises. Promises, like callbacks, are asynchronous. Unlike callbacks, they can be chained.

Promise takes 2 arguments and we need to resolve it - think of it like Promise's own way to return value when it is done.

new Promise((resolve, reject) =>
  resolve('Hello promise')
)
.then(value => console.log(value))

This then chain is really awesome, because now we can do something like this:

asyncReadFile('./file/to/location1.md', 'utf-8')
.then(value => {
  return anotherPromise
})
.then(value => {
  return anotherPromise
})
.then(value => {
  return yetAnotherPromise
})
// and so on

This looks MUCH better than callback spaghetti.

Putting the two together: replace all callbacks with promises

We learned two things:

  1. Too many callbacks leads to spaghetti code
  2. Chained promises are easy to read

However, callbacks functions are not the same thing as promises. fs.readFile do not return promises. We can't just use then chain on several fs.readFile together.

"Hmm, I wonder if there is a way to convert them callbacks into promises so I can chain them and make them look pretty?" - me thinking

Absolutely!! Promisify does JUST that.

Promisify is part of util built into Node 8+. It accepts a function that accepts a callback function (wow, that's a mouthful). The resulting function is a function that returns a promise. Let's jump straight into it. It will make sense after we run it ourselves.

Let's create several files in a directory that contains the name of other files to read. Then we will read the first file - see if we can make it to the last file.

// file1.md
file2.md

// file2.md
file3.md

// file3.md
Finished!

// reader.js
const fs = require("fs");
const { promisify } = require("util");

const promiseReadFile = promisify(fs.readFile);

promiseReadFile("file1.md", "utf-8")
  .then(content => {
    const nextFileToRead = content.trim();
    return promiseReadFile(nextFileToRead, "utf-8");
  })
  .then(content => {
    const nextFileToRead = content.trim();
    return promiseReadFile(nextFileToRead, "utf-8");
  })
  .then(content => {
    console.log(content.trim());
  });

Now let's node ./reader.js and see what happens. You should see "Finished!" printed.

Sweet! Now that is one spaghetti I don't mind eating.

delicious spaghetti

Javascript has another way to handle promises: async/await.

To test your understanding, can you convert promisified code above from then into async/await?

Thanks for reading. Happy hackin'! Let me know if you have questions!

Resources

  1. util promisify
  2. node8 util promisify is so awesome
  3. Promisification
  4. understanding nodes promisify and callbackify
  5. Promise docs
  6. callback functions in nodeJS
  7. Javascript and asynchronous magic
  8. Is JavaScript Synchronous or Asynchronous? What the Hell is a Promise?
© Copyright 2021 Igor Irianto