Callbacks, Promises and Async & Await in Javascript

23 Jan 2020


JavaScript started out as just the language for browsers. Its event driven model made it very appealing to write rich UX driven front-end webapps. It turned out that being "event driven" is exactly what the backend of a webapp needed to be too. Hence Node.js was born.

A primer

Let's see how an event-driven Javascript example snippet looks like. I'm going to take the example of a button click on a web page as an example.

<body>
  <button id='hello-btn'>Say Hello</button>

  <script>
    var btn = document.getElementById('hello-btn');
    btn.addEventListener('click', function() {
      console.log('Hello!')
    })
  </script>
</body>

When we put the above snippet into an html file and load it up on a browser, we'll see a button. Sure enough when we click on it, we'll see Hello! printed onto the console.

The interesting part in that snippet is

btn.addEventListener('click', function() {
  console.log('Hello!')
})

The above snippet is an example of a callback function. addEventListener(...) for an element, invokes(calls) a function(){ ... }, when a click event happens on that element.

Let's consider another example. You can put this snippet directly in your browser's console or put it in a .js file and invoke it with node file.js

setTimeout(function () {
  console.log('after 2 seconds')
}, 2000)

setTimeout(function () {
  console.log('after 3 seconds')
}, 3000)

setTimeout(function () {
  console.log('after 1 seconds')
}, 1000)

We see the execution coming out as

after 1 seconds
after 2 seconds
after 3 seconds

If you've programmed in JavaScript for long, this will not be a revelation. But for the sake of completeness, let me tell you what's happening. We mentioned that JavaScript is an event-driven language. Which simply put, when an event happens, trigger a function() {...}. The event wait 1 second happens first, followed by the event wait 2 seconds and finally the event for wait 3 seconds happens last. Since the JavaScript engine has no more events to listen for, it exits. To get an understanding of how, this works under the hood, watch this sweet talk on the event-loop by Philip Roberts.

The point here is, whenever there is an I/O bound operation, code written in JavaScript follows the above set pattern. You might have also noted that the above 3 setTimeOut(...)'s finished executing in 3 seconds.

Chaining with the callback pattern

Now what if in the above example, we want the execution to follow that exact order. Let's modify the snippet for just that, using the callback pattern.

setTimeout(function () {
  console.log('after 2 seconds')
  setTimeout(function () {
    console.log('after 3 seconds')
    setTimeout(function () {
      console.log('after 1 seconds')
    }, 1000)
  }, 3000)
}, 2000)

Sure enough, when we invoke it we see (after a whopping 6 seconds)

after 2 seconds
after 3 seconds
after 1 seconds

Well this is all fun and dandy, but I'm not gonna write something like that! But we all have!

Let's visit a JQuery snippet from the good ol' days to do an API call.

$.ajax({
  url: 'https://api.example.com/items',
  contentType: "application/json",
  dataType: 'json',
  success: function(result){
    console.log(result);
  },
  error: function(err) {
    console.log(err)
  }
})

Let's imagine we want to do another API call with the result we got from the first API call

$.ajax({
  url: 'https://api.example.com/items',
  contentType: "application/json",
  dataType: 'json',
  success: function(result){
    $.ajax({
      url: 'https://api.example.com/items/' + results[0]['id'],
      contentType: "application/json",
      dataType: 'json',
      success: function(another_result){
        console.log(another_result)
      },
      error: function(another_err) {
        console.log(another_err)
      }
    })
  },
  error: function(err) {
    console.log(err)
  }
})

Sure one can argue, that we could wrap the $.ajax({...}) function in a generic function and invoke that. But still the pattern would be

function(event, callback1) {
  callback1(event1, callback2) {
    callback2(event2, callback3) {
      ...
    }
  }
}

This slowly, but surely results in what JavaScript programmer's fondly call, callback hell. This also has the problem of correctly handling errors. Which callback threw an error, when there are more than 3 or 4 callbacks in the chain? One's attention level has to be over 9000 when writing code for correct error handling.

In order to combat this, Promises came and then async await followed.

If you've reached this far, I'm assuming you're in it for the long run. Let's tame this beast!

From this point on, we'll have just one example snippet to work with. We'll be invoking it with node app.js. In the course of this article we'll convert it from a callback to Promises and then finally to async await.

I initially thought of putting the content of app.js as below,

setTimeout(function () {
  console.log('after 2 seconds')
  setTimeout(function () {
    console.log('after 3 seconds')
    setTimeout(function () {
      console.log('after 1 seconds')
    }, 1000)
  }, 3000)
}, 2000)

for the callback example. As for Promises, app.js would look like

const afterXSeconds = (x) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (x === 1) {
        reject(x)
      }
      resolve(x)
    }, x * 1000)
  })
}

afterXSeconds(2)
  .then((data) => {
    console.log(`after ${data} seconds`);
    return afterXSeconds(3);
  })
  .then((data) => {
    console.log(`after ${data} seconds`);
    return afterXSeconds(1)
  })
  .catch((e) => {
    console.log(`execeptioned ${e}`)
  })

and for finally async & await, our app.js would contain,

const afterXSeconds = (x) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (x === 1) {
        reject(x)
      }
      console.log(`after ${x} seconds`)
      resolve()
    }, x * 1000)
  })
}

// note the async
const start = async () => {
  try {
    await afterXSeconds(2) // <- await here
    await afterXSeconds(3)
    await afterXSeconds(1)
  } catch (e) {
    console.log(e)
  }
}

start()

But this has no practical use case. So let's take up a library function that we commonly see and make it work for us.

Callbacks

Put the following in app.js.

const fs = require('fs');

const start = () => {
  fs.readFile('./r.txt', 'utf-8', (err1, data) => { // callback 1
    if (err1) {
      console.error(err1);
      process.exit(1)
    }
    fs.writeFile('./w.txt', data, (err2) => { // callback 2
      if (err2) {
        console.error(err2)
        process.exit(1)
      }
      console.log(`Wrote: ${data}`)
    });
  });
}

start()

Make sure you have a file called r.txt in the same directory as app.js with some content. Our example r.txt has a Hello World! followed by a new line in it.

$ cat r.txt
Hello World!
$

When we run the script we get a w.txt file.

$ node app2.js 
Wrote: Hello World!
$ cat w.txt
Hello World!
$

We read from a file called r.txt and created a file called w.txt with r.txt's content. Note how fs.writeFile(...) was invoked from fs.readFile(...)'s callback.

If r.txt is not found or node doesn't have any permissions to access it, err1 will be present. If w.txt can't be created by node, err2 will be populated. The script will exit in either case.

Promises

A function that returns a Promise will return either a positive or a negative handle for an event. Any asynchronous operation(ex: I/O) can succeed(positive) or fail(negative). A positive is returned when the resolve() function in a Promise is invoked. A negative is returned when the reject() function is invoked from it.

We'll now wrap the readFile() and writeFile() functions of the fs module in functions that return Promises.

const fs = require('fs');

const readFilePromise = (filePath) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, 'utf-8', (err, data) => {
      if (err) {
        reject(err)
      } else {
        resolve(data)
      }
    })
  })
}

const writeFilePromise = (filePath, data) => {
  return new Promise((resolve, reject) => {
    fs.writeFile(filePath, data, (err) => {
      if (err) {
        reject(err)
      } else {
        resolve()
      }
    })
  })
}


const start = () => {
  readFilePromise('./r.txt')
    .then(data => {
      return writeFilePromise('./w.txt', data)
    })
    .catch(e => {
      console.log(e)
    })
}

start()

Look how small our start() function is now. Remove w.txt and run node app.js again. We'll see our script works as expected.

Now that we've seen how to convert callbacks into promises, we'll see a utility function that comes with node.js(>8.0) that simplifies this. Replace the content of app.js with

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

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

const start = () => {
  readFilePromise('./r.txt')
    .then(data => {
      return writeFilePromise('./w.txt', data)
    })
    .catch(e => {
      console.log(e)
    })
}

start()

The promisify(...) function will take a callback as an argument and gives us a "Promisified" version of it. Neat huh!

Async & Await

You know the dance. Replace the content of app.js with

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

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

const start = async () => { // note the async
  try {
    const data = await readFilePromise('./r.txt');
    if (!data) {
      console.log('Could not read from file')
      return;
    }
    await writeFilePromise('./w.txt', data)
  } catch (e) {
    console.log(e)
    return;
  }
}

start()

Remove w.txt and run node app.js. The gist about async & await is, if a function fx returns a Promise, function fx can be called from within an async function fy. If fx's resolve() function returns something, that value will can be stored in variable (ex: res)

The syntax is

const fy = async () => {
  try {
    const res = await fx()
  } catch (e) {
    ...
  }
}

By using this approach we have all our operations running in the try{...} block, and if something goes wrong, the main catch(){...} block will deal with it.

Fin

Phew! that was a long read. Thank you for reaching this far. Hopefully, we now have a good grasp on callbacks, promises and async/await.

If something was unclear or you my dear reader thinks that something needs improving, hit me an email. I'll gladly hit you back.

Happy Hacking & have a great day!