intro to async/await in js

Note: though this tutorial uses JavaScript, the same principles apply to other languages as well, such as Python/C#.

Why do we need Async/Await in the first place?

Before Promises and Async were introduced to JavaScript, all operations that would take some time and block the main thread resulted in what is known as callback hell. For example, let us imagine that you need to make a request to the API that takes roughly 2 seconds. This is what you would do:

const getResource = callback => {
    setTimeout(() => {
        callback('some resource');
    }, 2000);

getResource(resource => console.log(resource));

In this snippet we pass the function on line 7 as an argument to our getResource function and it will call it with requested info after 2 seconds. Does not look so bad, does it? It gets way worse as your code grows, you get multi-level nested callbacks and something that looks like this:

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)

This code becomes harder to read, maintain, and extend. If you want to learn more about callback hell, visit


In attempt to fix the callback hell , Promises were introduced in ES2015. Their goal was to simplify the code, improve readability and add more asynchronous functionality to JS. With promises, the code in the first example becomes:

const getResource = new Promise((resolve, reject) => {
    setTimeout(() => resolve('some resource'), 2000);

getResource.then(resource => console.log(resource));

We create a Promise object on line 1. We pass a function as an argument which receives resolve and reject functions. In it, we make a call to our api and resolve the Promise with desired content. In an event of an error, we reject the Promise with some meaningful error message. Then, we call this promise by passing a callback function in .then(). To handle an error, we would also pass a callback function in .catch().

While this is definitely a step forward, it is not a complete solution. We still have callback functions, chaining and a lot of redundant code. There is also such a thing as promise hell, though it can be avoided using techniques outlined in this post by @pyrolistical.


Async functions were introduced in ES2017 and are now compatible with all browser specifications except IE. An Async function is simply a function that returns a Promise, meaning, it should be waited for. We can still use await with all of promises written in legacy wait as they are backwards-compatible. Look at our example with promises:

const getResource = () => new Promise((resolve, reject) => {
    setTimeout(() => resolve('some resource'), 2000);

main = async () => {
    const resource1 = await getResource();
    const resource2 = await getResource();
    console.log(resource1, resource2);


This example is a bit longer, but do not let yourself be confused by this: async functions almost always win in terms of lines of code. Here we firstly define our promise and note that it slightly differs: now it is a function that returns a promise: I did it to show you how could we use the same promise twice. Later we define an async function main, because we can only use await keyword in async functions. Then, we fetch resources from our promises by using await. Note that this will not block neither the main thread nor the UI thread, hence the word async. You can see we have less callbacks now and if we wanted to hadle errors we would wrap the await statements in try/catch/finally blocks, just as with any other language. So the winnings in terms of readability and simplicity are obvious: the async code looks more like normal, synchronous code unlike callbacks/promises.

Please feel free to reach out with corrections/suggestions, that would be of great help and mean a lot to me ;). Happy coding!

Get new content delivered to your mailbox:

leave a comment