Easy async/await in JavaScript

Easy async/await in JavaScript

A not-so-long time ago, JavaScript was renowned for a thing called callback hell. I should know, I've written projects which would have nested levels of callbacks. For example:

function saveWebPage(url, callback) {
  // Get the web page
  getWebPage(url, function(err, html) {
    if (err) callback(err);
    
    // Load it if exists
    loadPage(url, function(err, page) {
      if (err) callback(err);
      
      if (page === null) {
        // Insert it
        insertPage(url, html, function (err) {
          if (err) callback(err);
          callback(null, html);
        })
      } else {
        // Update it
        updatePage(url, html, function (err) {
          if (err) callback(err);
          callback(null, html);
        }
      }
    });
  });
}

Yuk 🤮! It would be messy. And worst part was, if you needed do to an if statement, you had to do one of two things. Create functions for each branch, or duplicate code inside the if statement. The cognitive overhead was too much.

Introducing Promises

Then Promises came along. They were a neat way to chain things together. But they weren't exactly the easiest to remember how to write. The above example would be written something like this:

function saveWebPage(url) {
  var pageHtml = null;
  return new Promise(function (resolve, reject) {
    getWebPage(url).then(function (html) {
      pageHtml = html;
      return loadPage(url);
    }).then(function (page) {
      if (page === null) {
        return insertPage(url, pageHtml);
      } else {
        reutrn updatePage(url, pageHtml);
      }
    }).then(resolve)
    .catch(reject);
  });
}

Using Promises, you can simplify your work by returning a Promise object that will either resolve or reject. Resolve means the work was successful and it has a value to return, and reject means there was an error.

In our example, we have chained everything together. It's one step away from callback hell that we had in our first example.

But honestly, I find this very cumbersome to write and can never really remember how to chain, when to do a new Promise and how to return values from one then call to the next.

Async/Await

JavaScript introduced two new keywords async and await which extends the behaviour of Promises but without having to write the full verbose Promise code.

const saveWebPage = async (url) {
  const html = await getWebPage(url);
  const page = await loadPage(url);
  if (page === null) {
    await insertPage(url, html);
  } else {
    await updatePage(url, html);
  }
}

Under the covers, the await keyword simply blocks the current JavaScript execution context on a Promise and waits for it to either resolve or reject. Unlike other blocking in JavaScript (like calling fs.readFileSync), other things can still run, such as other callbacks listening to network ports, file reading, database calls, etc.

It's a great way to simulate a synchronous threads in an event driven programming language, still without using threads like in Java.

How to call an async method

You may find yourself needing a simple script to run that uses await. But you can't use await if it's not inside an async method. How do we run an asyncmethod? Here's how:

import { saveWebPage } from "./mylib.js";
const url = process.argv[0];

console.log('Saving ' + url);

(async () => {
  try {
    await saveWebPage(url);
    console.log("Web page saved");
  } catch (ex) {
    console.error("Unable to save web page:", ex);
  }
})(); // Call our async method immediately

Basically we've wrapped our async method in brackets and called it immediately. Now we are free to use await 🕺.

async/await with classes

You may have a situation where you have a class and you want to have async methods on it. You can do something like this:

class WebPageDownloader {
  async getWebPage(url) {
    return await getWebPage(url);
  }
}

(async () => {
  const downloader = new WebPageDownloader();
  const html = await downloader.getWebPage('https://zaro.io');
  console.log('Downloaded', html);
})();

The above demonstrates that you can add an async keyword to a class method and await on it. You can also add it to static methods as well:

class WebPageDownloader {
  static async getWebPage(url) {
    return await getWebPage(url);
  }
}

await WebPageDownloader::getWebpage('https://zaro.io');

await on any Promise

One neat trick is that you can await on any Promise object. For example:

const sleepFor60Seconds = new Promise((resolve, reject) => {
  setTimeout(resolve, 60000);
});

await sleepFor60Seconds;

Which means that async IS a Promise object 🤯!

async IS a promise

The magic piece of information is that when you have a function that is an async function, this is just shorthand for it being wrapped in a new Promise and the return value is passed to the resolve and the exceptions are passed to the reject. This means you can mix and match the use of promises and async methods.

For example:

// Await on Promise object
const promise1 = new Promise((resolve, reject) => {resolve('success'});
await promise1;

// Await on Async function (which is a promise object)
const promise2 = async () => {return 'success'};
await promise2;

Notice that await waits on a promise object, not on a function? This means that when we use it for a function, two things are happening. Firstly, we're executing the async function immediately which only generates a Promise object. Then it waits on the promise object.

const result = await getResult();

// Is the same as
const promise = getResult();
await promise;

Older browsers

Async functions where only introduced in Microsoft Edge and IE11 does not support it. There is generally good support across other browsers. The good news is, you can compile this to older versions of JavaScript using Babel. But that is outside the scope of this article.

In summary

Since using async/await, I now use JavaScript and Node.js for almost all forms of scripting. Previously I would use PHP because it is synchronous by nature and everything blocks. Using Node.js in the early days was cumbersome and had callback hell. But now it's quick and elegant to write code that is very expressive with minimal code.