Asynchronous Javascript: From Promises to Async/Await

Javascript is a funny language. It claims to be (and very much is) a single-threaded language (i.e., it executes statements in order, one at a time, one after another, in a synchronous fashion). Despite just having the one native thread to work with, it somehow allows you to write concurrent, asynchronous code that is non-blocking in nature.

Over the years, Javascript has provided different ways to leverage this asynchronism through its various ECMAScript (ES) editions. It all started with callback functions that allowed you to define asynchronous behavior through functions passed to other functions as parameters. This was then made easier by introducing Promises, also known as the Futures API. Promises are objects that asynchronously function to return a value sometime in the future. Following this, with ES 2017, the Javascript community sprinkled some syntactic sugar and sparkle over the Promises API to introduce the async/await syntax. The aim here was to make the process of writing asynchronous code easier, simpler, and more intuitive. 

As Javascript has tried to put forth various upgrades to its asynchronous toolkit, there has been an air of confusion during this transition phase: about how to, and whether to switch to async/await or to stick with promises, or consider some hybrid setting. This is something a lot of web dev groups have struggled with over the last few years. It has also been difficult for beginners and newbies to wrap their heads around three different ways of doing the same thing - to understand how they vary and what advantages one provides over others. Through this post, I wish to clear the confusion about all of this while working towards building a solid contextual understanding of the basics of asynchronous programming in Javascript.

In this post, we will understand about asynchronous programming in the web: about why non-blocking code is necessary for creating a fast website in a world of sluggish API calls and network requests. We will talk about how Javascript achieves asynchronism and concurrency under the hood and about the history of asynchronous support provided by the language. Most importantly, we will discuss in-depth about Javascript Promises and the newer async/await syntax: about the advantages of using one over the other and about how you could switch from a Promises-based code to one that uses async/await. Here are the key questions I wish to target through this post:

Let’s get started!

The Fundamentals of Asynchronous Javascript

In this section, we will discuss the fundamentals of asynchronous programming by comparing it against synchronous programming paradigms for the web. We will look at how Javascript allows asynchronism under the hood, along with a brief history of the various ways to write non-blocking code.

Synchronous and Asynchronous Programming

Synchronous programming

Operations in synchronous paradigms happen one at a time, one after another: each line (or block) of code needs to wait for the one before it to complete. There’s usually only one thread to keep track of the code statements in such cases. 

Synchronous programming can stifle web applications the most. This is because of the very many tasks a web app needs to take care of - network requests and API calls for fetching web page content, exchanging data with external APIs, user authentication, WebSockets, and much more. Since sluggish network requests can effectively bottleneck your application, the synchronous way of doing things can substantially hamper performance. 

Writing pure synchronous code that is blocking and halts execution while waiting for a specific task to complete can intermittently freeze your web page, disallowing any user actions. This obviously results in a poor experience for the user.

undefined

We, therefore, need to be able to run our code in a non-blocking way. Let us look at what asynchronous programming has to offer in this regard.

Asynchronous programming

Asynchronous code operations on the other hand are non-blocking in nature. They ensure that you don’t need to wait for the lengthier operations to complete before your code can move ahead to the remaining statements.

With asynchronous programming, any part of your code that takes longer to run can be handled by a parallel entity that does not block the main code thread. These lengthier and time taking operations can return their output to the main thread whenever they are ready. As a result, we don’t need to keep waiting for them to complete before we move ahead in our code. 

undefined

As you can see from the synchronous vs. asynchronous programming diagram, this allows us to get much more stuff done in a short amount of time and therefore can be very helpful in web applications.

Asynchronous Programming in Javascript 

Javascript is a single-threaded programming language. It natively has one call stack and one memory heap at its disposal. This seems to imply that all operations need to pass through that main thread and therefore wait for the preceding ones to have completed before they can get their chance.

So how does Javascript allow concurrency? How and where are the blocking pieces of your code handled while the rest of it continues to execute smoothly? How is everything synchronized?

These are some very interesting questions if you are keen on understanding how Javascript manages all these multiple tasks under the hood. The answers to these questions lie in the nitty-gritty of how Javascript code is run. Let’s try to understand it.

Each time you run Javascript code, there are five primary components that work towards its execution. There is the Javascript Engine (or the JS runtime) and there is the runtime environment (with its various APIs). Javascript can run in many environments, but for web development inside the browser, we can talk about the browser runtime environment. Apart from these runtimes, there is also the call stack, the callback queue, and the event loop

Note: There is also the memory heap, but we can leave that for discussing asynchronous Javascript.

3.png

Javascript Engine 

The JS engine is essentially a C++ program that is responsible for converting your Javascript code to machine code and executing it. It is a virtual machine that interprets and executes JS code. Some common examples of JS engines are - Google Chrome’s V8, Mozilla’s SpiderMonkey, and Internet Explorer’s Chakra.

4.png

The thing with the JS engine is that it only allows you to plainly run Javascript code. Even though its importance can’t be understated, there’s a lot more your website needs to turn on its charm - i.e., to be able to connect and talk to the internet, to leverage the browser interface, etc. Let’s see how that is made possible.

Browser Runtime Environment

This JS engine runs inside a bigger container: the runtime environment. Each browser has its runtime environment. This is where quite a lot of the web development magic happens.

The browser runtime provides you with a bunch of utilitarian tools that are pivotal to web development. It exposes multiple web APIs for developers to effectively utilize the browser environment. For example, the DOM API allows you to manipulate your web page elements, XML HTTP Request (XHR) support for communicating with servers over the internet, and much more. All of this is not a part of the native JS engine, but instead is provided by the browser’s runtime environment.

5.png

Among the many advantages of web APIs, they also facilitate concurrency in your application. Let’s see how.

Call Stack, Callback Queue, and Event Loop

The Javascript call stack is a synchronous data structure that keeps track of the function calls in your code. When a function is called, it is pushed to the call stack and when it is done executing, it is popped out.

When you call a function like setTimeout that would otherwise have been blocking in nature, after being pushed to the call stack, it is immediately transferred to the browser’s web APIs. These web APIs take up the responsibility of waiting for the operation to complete (in this case, waiting for the timeout period to complete). While these web APIs are tracking the status of the asynchronous operation, the call stack will continue the execution of the rest of the items in the call stack. 

Once your setTimeout function is completed, it is pushed to (what is known as) the callback queue or the task queue. This queue stores the asynchronous tasks that have been completed before they are pushed back to the call stack to be finally executed.

The event loop is the last part of this equation that is responsible for pushing the items waiting in the callback queue to the main call stack. It closely monitors the call stack and the callback queue - when the call stack is empty, it picks the topmost item from the callback queue and pushes it to the call stack for execution.

For a deeper understanding of these components, I would recommend you to check out this renowned talk on Event Loops by Philip Roberts at JSConf EU 2014. Here is a visualization of how asynchronous code is run under the hood from his talk:

undefined

Asynchronous execution in your Javascript code is orchestrated when these components work in synchrony (pun intended). This is what enables you to write non-blocking, asynchronous code for your application.

A Brief History of Asynchronous Javascript

Now that we know about the internal mechanics of how code is executed, let us look at the API that Javascript provides for programming asynchronous behavior in our applications. There’s a brief, but dynamic history of Javascript functions and implementations that enable you to write asynchronous, non-blocking code. 

Callback functions 

It all started with callback functions, which are used like this:

function myCallbackFunction () {
   console.log("Hello world 🌎")
}
      
setTimeout(myCallbackFunction, 2000)

Here we have defined a callback function (myCallbackFunction()) that we want to be called after 2 seconds. The setTimeout function takes in the callback function as a parameter to execute when the specified time period is over. 

Callbacks were much more prevalent earlier for writing asynchronous network requests. The problem with this was realized when you had too many asynchronous operations to chain together; it resulted in what is infamously known as the callback hell. Let’s look at what this is through an example.

Let’s say we want to do three GET requests (using JQuery), one after another, in a way that doesn’t block the rest of the code. We can do this with callbacks using so:

<script>
    function get_data() {
        $.get('https://url.com/one', () => { // using js arrow functions
            $.get('https://url.com/two', () => {
                $.get('https://url.com/three', (res) => {
                    console.log(res)
                })
            })
        })
    }
</script>

As the number of chained operations increase, this can quickly become very unwieldy, unmanageable, and therefore difficult to manage. 

Javascript Promises 

To make this easier, with ES 6 (ES 2015), Promises were introduced in Javascript. They allowed more flexibility, better readability, and more control over how you wanted your asynchronous code to be handled.

As you saw in callbacks, we passed the callback functions as a parameter to another function. With promises, however, you could define the post-completion behavior by attaching a function to the returned promise object using the .then() function. We will understand this in more depth later in the post.

Before that, let’s take a glimpse at how we could write the code for the previous example using Promises:

<script>
    function get_data() {
        $.get('https://url.com/one') // chained using .then()
        .then(() => $.get('https://url.com/two'))
        .then(() => $.get('https://url.com/three'))
        .then((res) => console.log(res))
    }
</script>

Note: I am going to use JQuery’s GET requests ($.get) throughout the post because it returns (and therefore supports) Promises.

In this example, each JQuery GET request returns a promise, upon completion of which the GET request inside .then() is executed and so on and so forth. This is a very arbitrary example to demonstrate how easily you can chain asynchronous events in a way that is easier to read, manage, and debug. Compare this with the more obscure code we saw in the previous example. 

Now let’s look at how Promises were improved upon by introducing the async/await syntax in Javascript.

Javascript Async/Await 

Apparently, there was even more room for improvement in Javascript’s asynchronous support. With ES 8 (ES 2017), the async/await syntax was introduced to simplify things even more. Async/await serves mostly as syntactic sugar on top of Promises, but has brought about a decent change in how code is written and in a bunch of other things that we’ll discuss later in the post.

Just so you can get a glimpse of how easy and minimal it has become to work with Promises, here the previous example is written using the async/await syntax.

<script>
    async function get_data() { // async function
        await $.get('https://url.com/one') // execution pauses, waiting for request to complete
        await $.get('https://url.com/two')
        let res = await $.get('https://url.com/three')
        console.log(res)
    }
</script>

The difference is clearly visible. Instead of chaining all subsequent operations with .then(), we can just write them as different statements, line after line. Adding await before a statement (inside an async function) makes Javascript pause the execution inside the function and wait until that operation is completed. 

All the three code snippets we saw above do the same thing, but you can see how some of those are much easier to read, maintain, and debug than others. 

We have only skimmed the surface so far. Let’s deep dive into the latter two, more commonly used methods these days - Promises and async/await.

Javascript Promises 

Promises are Javascript objects that represent an “eventual completion (or failure)” of some asynchronous code. It stands for an operation that hasn’t completed yet, but ‘promises’ to run asynchronously and return a value (or information about its failure) when it’s done.

It allows you to write asynchronous code in a synchronous manner and can take one of three states: 

undefined

A promise always starts off with the ‘pending’ state. Then based on whether it completes successfully or is rejected (fails or throws an error), it’s state changes to ‘fulfilled’ or ‘rejected’. 

The Promises (or the Futures) API allows you to program your asynchronous behavior corresponding to each of these cases: you can capture the fulfillment case by using the .then() function and handle the rejections and thrown errors using the .catch() function.

You can also initialize your own Promises if you want. Here is an example of how to implement a custom Promise object, how to resolve or reject it with a response, and how to define subsequent behavior:

  <script>
       let a = 1;
       var myPromise = new Promise((resolve, reject) => {
           if (a > 0) {
               resolve(a)
           } else {
               reject("My error message")
           }
       })
    
       myPromise.then((res) => { // if promise is fulfilled
           console.log('Promise was resolved ✅')
           console.log('Returned value:', res)
       }).catch((err) => { // if promise fails
           console.log('Some error occured! ⚠️')
           console.error('Error message: ', err)
       })
  </script>

OUTPUT:

undefined

The good thing about .then() and .catch() functions are that they also return promises. This can be helpful if you want to chain a bunch of asynchronous operations together. Since .then() is used to act on resolved promises and it itself returns a promise, we can put multiple .then()functions one after another as per our requirement. 

Here is an example code snippet where we do three consecutive requests and print the response of the last one:

<script>
 
   let url1 = 'https://jsonplaceholder.typicode.com/posts/1'
   let url2 = 'https://jsonplaceholder.typicode.com/photos/1'
   let url3 = 'https://jsonplaceholder.typicode.com/comments/1'
 
   $.get(url1)                 // get url1
    .then(() => $.get(url2))   // then url2
    .then(() => $.get(url3))   // then url3
    .then((res) => console.log(res)) // print response
    .catch((err) => console.error(err)) // error handling
	
</script>

This is just an arbitrary example where I have run three GET requests, one after another to a public JSON API for testing. The code here is just to give you a simple understanding of how multiple asynchronous events can be chained.

OUTPUT:

undefined

Ever since promises have been introduced, many third-party libraries have allowed developers to leverage it by making their functions now return promise objects that you can process and chain using the .then() function. Before this, people had to work with clunky callback functions that made the code more convoluted and relatively difficult to maintain and debug.

Javascript Async/Await 

Promises were great to work with, but there was room for improvement. The Javascript (TC39) committee wanted to put forth a more minimal and convenient interface to write asynchronous code. 

The promise class and objects were happily continued, but there was a significant change in the way you could work with and chain multiple events. This was made possible through the new async/await syntax introduced in ES 8 (ES 2017). Even though considered to be mostly syntactic sugar, it has made the process of managing asynchronous code much more intuitive and simple. Let’s look at how it works- 

Async Functions

Async functions are functions that return a promise. You can declare any function as an async function just by adding the ‘async’ keyword before it. These functions were realized to obviate the need for explicitly instantiating new custom Promises. In the below example, let’s see how we can create an async function and how it returns a promise or a value.

<script>
 
   async function f() {
       return "Hello world! 🌎";
   }
   console.log(f()) // prints the promise
   f().then((res) => console.log(res)) // prints returned value
 
</script>

As you can see, the function f() returns a promise. We can chain subsequent actions and work with the returned value by using .then() function. 

Since we wouldn’t want to write async functions just for returning basic strings, let’s look at how we could utilize the functionality better.

Await statements

Earlier, if you wanted your code to wait for a network request (or any blocking code) to complete before it moved to the next task, you would have to wrap that code inside a .then() callback function. 

So in a way, only the code that was wrapped in .then() would wait for its preceding (promising) lines. 

To make information about asynchronous operations more explicit and intuitive to the developer, the await keyword has been introduced. By adding the await keyword to a statement, we essentially instruct the execution (inside the async function) to pause and wait for the promise to be fulfilled. It is very important to note here that we can use the await keyword only inside an async function. 

Let’s rewrite the previous Promises’ example using async/await:

<script>
 
   async function get_data() { // using an async function
       let url1 = 'https://jsonplaceholder.typicode.com/posts/1'
       let url2 = 'https://jsonplaceholder.typicode.com/photos/1'
       let url3 = 'https://jsonplaceholder.typicode.com/comments/1'
 
       await $.get(url1)                 // get url1
       await $.get(url2)                 // then url2
       let res3 = await $.get(url3)      // then url3
 
       console.log(res3) // print response
   }
 
   get_data()
 
</script>

As you can see, readability has improved comprehensively.

await basically halts execution of the code within the async function inside which it is located. It implies that the code would first wait for the await statement to complete executing - for the promise to be resolved (or rejected), and would only then move to the next line. This is similar to what happened when the subsequent statements were wrapped in the .then() function. 

Also, as expected, our async function is non-blocking in nature: as it waits for the Promises to be resolved, the remaining code (outside the async function) continues to run.

One good thing about Javascript is that, in this case, you don’t need to strictly stick to one way of doing things. You can use the .then() function with await statements too. Below is an example where we use the .then() function to define the action after the third GET request is completed.

<script>
   async function get_data() {
       let url1 = 'https://jsonplaceholder.typicode.com/posts/1'
       let url2 = 'https://jsonplaceholder.typicode.com/photos/1'
       let url3 = 'https://jsonplaceholder.typicode.com/comments/1'
 
       await $.get(url1)                 // get url1
       await $.get(url2)                 // then url2
       await $.get(url3).then((res) => console.log(res)) // using await with .then()      
   }
 
   get_data.then(() => console.log(“Hello world! 🌎”))
</script>

Like we discussed above, async/await is basically a wrapper on top of the Promises API. It also returns promise objects. In the above example, since the async function (get_data()) returns a promise, we can plug a .then() function to it to define the subsequent operation.

Async/Await vs. Promises

In this section, we will be comparing async/await and Promises to get a more fine-grained understanding of how the former convincingly outperforms the latter in various aspects.

Brevity, Readability, and Simplicity

“Write clean code and you can sleep well at night.” - James Richman

This is the most visible and therefore convincing advantage of switching to the async/await syntax. It allows you to write clean, minimal code that doesn’t need unnecessary .then() functions (to wrap asynchronous code) that make it relatively difficult and complex to maintain. 

This is why async/await is very commonly referred to as syntactic sugar. In fact, over the years various ECMASCRIPT editions have prioritized updates that allow developers to write code that is easier to read, natural, and more intuitive. This can be seen in Javascript arrow notation functions, in the switch from conventional callback functions to .then() and .catch(), async/await, and much more.

Scope { }

One of the major differences between the Promises and async/await is their asynchronous scope. 

In Promises, we are not bound by any wrapper async function to write asynchronous code. We can write our asynchronous code however we like, wherever we like. Because of this, any code that is not wrapped in a .then() function, will not wait for the preceding promises for execution. We can see this in an example here:

<script>
 
   let url = 'https://jsonplaceholder.typicode.com/posts/1'
   $.get(url).then((res) => console.log(‘Response ->’, res))
  
   console.log("Hello world! 🌎") // this will not wait for async request to complete
 
</script>

OUTPUT:

undefined

As you can see, the print statement did not wait for the asynchronous GET request to complete before being executed. 

On the other hand, with async/await, the whole async function’s scope is asynchronous in nature i.e. when an asynchronous operation is taking place in the async function, every statement following that operation will have to wait for it to complete (for the promise to be resolved) before moving ahead.

This is unlike what happens with Promises where only operations included in the (Promise) chain is handled asynchronously. Let’s implement the above code in an async/await setting and see the output.

<script>
 
   async function get_data() {
       let url = 'https://jsonplaceholder.typicode.com/posts/1'
       let res = await $.get(url)
       console.log('Response -> ', res)
  
       console.log("Hello world! 🌎") // this will have to wait for async request to complete
   }
  
   get_data()
   console.log("Hello world! 👋🏽") // this will not wait for async function to complete
 
</script>

OUTPUT:

undefined

Here you can see that the print statement inside the async function was executed only after the await statement had completed. This is because await forces the code to pause at that line in the function until the promise has resolved. Code that is waiting outside the scope of the function continues to run, but inside the function it will pause at await statements.

Ideally, there’s no good or bad way of doing things here. The choice is yours. You can choose whatever works better for your use case.

Chaining 

We discussed above how chaining a bunch of asynchronous events with Promises and .then() can get quite unwieldy. Let’s see this through a more common example.

We’ll consider a case where we want to make three consecutive asynchronous GET requests and log their responses. This is what the code with Promises would look like:

   $.get(url1)
    .then((res1) => {
        console.log(res1)
        return $.get(doSomethingWithRes(res1, url2))
    })
    .then((res2) => {
        console.log(res2)
        return $.get(doSomethingWithRes(res2, url3))
    })
    .then((res3) => {
        console.log(res3)
    })

Thanks to Javascript arrow notations, this doesn’t look too bad. However, it could be much more simple to implement with async/await, where all consecutive await statements inside the async function are automatically chained. This is what our code would look like:

   async function myFunction (){
       let res1 = await $.get(url)
       console.log(res1)
       let res2 = await $.get(doSomethingWithRes(url, res1))
       console.log(res2)
       let res3 = await $.get(doSomethingWithRes(url, res2))
       console.log(res3)
   }

As you can see, chained asynchronous events are much easier to read, write, and maintain with async/await. Since async functions treat each awaitstatement as a promise, you can instead make your asynchronous operation calls line by line, making it much easier to work with.

Error handling

As far as error handling is concerned, the try/catch block mechanism doesn’t work for handling errors thrown from Promises.  We need to rely on Promise’s .catch() function for this. Let’s look at an example to understand this better:

try {
       $.get(url1)
        .then(() => $.get(url2))
        .then(function (res){ throw new Error("Hello error! ⚠️")}) // throwing error to see if it’s caught
       // uncomment below line ⬇️ to handle above thrown error
       // .catch((err) => console.error('Handling error: ', err))
     
   } catch(err){  // this won't be able to capture any errors that happen inside promises
       console.error(err)
   }

Here, our thrown promise would not be captured and handled by the catch block unless we use the .catch() function for our promise. This is however not the case with async/await systems. Let’s see how the code for that looks:

  async function myFunction () {
       try {
           await $.get(url1)
           await $.get(url2)
           throw new Error("Hello error! ⚠️") // throwing error to see if it’s caught
       } catch(err){  // this will be able to capture any errors that happen inside our async function
           console.error(err)
       }
   }

In this case, your thrown error will be caught by the catch block. This is better because now you don’t need to worry about two different methods for catching your errors (.catch() for promise related errors and try/catch for all other errors) and can use the conventional try/catch method for all errors.

In earlier versions of Javascript, the error stack trace error of purely promise-based code was also slightly misleading in its report. This has apparently been fixed now.

IDE support

By now you should be pretty convinced about how much better async/await is. Even the most popular IDEs seem to agree with this. Therefore ones like Visual Studio Code support developers in switching from .then() based promise code to async/await syntax. 

In Visual Studio Code, if you write Javascript code that uses the .then() method, it automatically suggests you switch to the (apparently) better way of doing things. Here is a GIF showing how you can utilize this feature.

undefined

Conclusion: When to Use What & How to

It is important to know that when we use async/await, technically, we’re still working with promises: the async function returns a promise and the keyword await waits for that promise to resolve. However, when the internet talks about using async/await over promises, they are talking about not relying on the .then() method of defining asynchronous behavior, and instead of writing async functions that use awaitstatements.

As we have seen in the last few sections, the advantages of async/await clearly outweigh those of promises. Having said that, an intelligent hybrid of the two can also be considered for your implementation. For instance, sometimes when you need to make just one or two async calls in your code, creating a separate async function can be considered overkill. In that case, you can instead just use the .then() function to program your async behavior.

Another thing to keep in mind about async functions is to not write very long ones. This is because the whole scope of an async function is put to hold when an await statement is encountered. This can be limiting in many cases as it makes your code more reliant on those promises to fulfill quickly for the rest of the execution to go about smoothly. So it is considered to be a good practice to have multiple, smaller async functions for better utilizing these asynchronous mechanisms.

Converting from Promises to Async / Await

Now, to recapitulate our understanding of how the async/await syntax can be used, let’s discuss the process of converting a Promise-based code to one that uses async/await. The specific changes required would be different for different applications, but let’s highlight the fundamental steps that can be applied in any scenario.

Step1: Understand the behavior you want to achieve

The first step is to understand how you want your code to behave: to understand which operations are asynchronous, which are synchronous, the sequence, which operations need to wait for which to complete, which are independent, which are not, and so on.

Step2: Create separate async functions for chained promises

Then, create async functions for porting your promise chains. There is no hard and fast rule here about how many async functions to create for n promise chains, but a good rule of thumb is to have multiple smaller async functions as opposed to a few lengthier ones.

Step3: Move code from .then() to separate await statements

The next step is to unwrap the .then() functions from your promises and move them to the async functions we just made, separating one promise from another using await statements. 

For any code that you move to the async function, it is important to remember that inside an async function, execution will move in a sequential manner, line by line, and that await statements will be waited upon before execution moves ahead to other statements that might not want to wait.

Step4: Use try/catch for error handling

Like we discussed above, since errors thrown from async functions and await statements can be handled by conventional try/catch blocks, we do not need separate .catch() functions as we did for promises.

As we mentioned above, IDEs like Visual Studio Code can make this whole conversion process easier by letting you switch to async/await with just a click of a button.

Now It’s Your Turn

To summarize - in this post, we talked about asynchronous programming in Javascript. We discussed synchronous and asynchronous paradigms, and about the importance of writing non-blocking code for your web application. We then took a sneak peek at how asynchronous code works under the hood in Javascript. We also looked at the different APIs that JS has provided to write asynchronous code - callbacks, promises and async/await. We primarily delved deeper into the latter two, learned about how they differ and compare against each other. We also discussed the many ways in which async/await fared better than pure promise-based and finally, the steps required to switch to the slightly better way of doing things.

Now it’s time for you to implement your understanding of asynchronous programming. Go ahead and start using async/await for writing asynchronous code for your web application and make the internet a better place! Good luck!