DEV Community

Cover image for A Practical guide to Async and Await for JavaScript Developers
Yusuf Mubaraq
Yusuf Mubaraq

Posted on

A Practical guide to Async and Await for JavaScript Developers

Introduction

Async and Await are JavaScript keywords introduced in ECMAScript 2017 (ES8) that enable writing asynchronous code in a more readable, synchronous-like, and manageable manner. They simplify handling operations that take time to complete, such as fetching data from an API.

Before we dive in, let’s first understand the concepts of synchronous and asynchronous programming in JavaScript. In synchronous programming, tasks are executed sequentially, one after the other, in the order they appear. Each task must complete before the next one begins. On the other hand, asynchronous programming allows tasks to run in the background, enabling JavaScript to continue executing other tasks without waiting for the previous ones to finish.

As we know, JavaScript is a single-threaded language, meaning it can only execute one task at a time. If that's the case, how does JavaScript handle asynchronous code? This is made possible by the Event Loop, a key mechanism that works alongside the JavaScript runtime environment. The event loop enables asynchronous operations to run without blocking the main thread, ensuring that JavaScript stays responsive. Without further delay, grab a cup of coffee and let’s jump right into today’s topic!

What is Async?

To better understand this concept, we’ll take a more practical approach. Before the introduction of async and await, Promises were handled using the "old way," which was introduced in ES6 (ECMAScript 2015). Let’s explore the example below.

Old promise

The code above demonstrates the traditional syntax for handling Promises. The Promise constructor is used to create a new Promise instance. It accepts a function (known as the executor function) as its argument, which includes two parameters: resolve and reject. This executor function contains the logic for the asynchronous operation. In this example, resolve is called immediately, signaling that the Promise has successfully completed with a specific value. Once the Promise is resolved, the .then method is triggered, executing its callback to log the result.

However, this syntax can be somewhat tricky to remember. The introduction of async/await simplified the process of handling promises, making it much easier to read and understand. Let’s look at an example below.

new-way
To implement an async function, we use the async keyword, which tells JavaScript that this is not a regular function but an asynchronous one. The second example shows how the same can be done using an arrow function.

Another important concept to note is the await keyword. Both async and await work together to simplify handling Promises. The awaitkeyword can only be used inside an asynchronous function—it cannot be used outside a function or within a regular function. It must always appear within a function marked as async. Now that we’ve covered the basics, let’s dive deeper into the concept!

How It Works Behind the Scenes

Many JavaScript developers use async/await regularly in their code, but only a few truly understand how it functions under the hood. That’s where this tutorial comes in. Let’s explore an example to break it down.

then-example

In this example, we use the .then method to better understand how Promises work compared to the async/awaitapproach. When the handlePromise() function is called, the code executes line by line. When JavaScript encounters the .then method, it registers the callback to the Microtask Queue and immediately moves to the next line, printing 'hello world'.

Once all synchronous tasks are completed, the JavaScript engine checks the Microtask Queue for pending tasks. After five seconds, the setTimeout completes, and its callback is pushed back to the call stack. At this point, the Promise is resolved, and the registered callback runs, logging the result.

In short, the JavaScript engine doesn’t wait, it moves directly to the next line of code. Now, does the same behavior apply when using async/await, or does it work differently? Let’s find out!

example
In the example above, when the handlePromise() function is called, the first line 'the start' is printed. JavaScript then encounters the awaitkeyword, which indicates that the function is asynchronous and involves a Promise. It identifies that the Promise will take five seconds to resolve due to the setTimeout. At this point, the handlePromise() function is suspended (removed from the call stack), and any code after the await inside the function is paused.

The JavaScript engine continues to execute the rest of the program. After five seconds, the Promise is resolved, the suspended function is returned to the call stack, and the remaining lines inside handlePromise() 'Promise is resolved' and 'the end' are executed in sequence.

It’s important to note that suspending the function doesn’t block the main thread. If there’s other code written outside the handlePromise() function, it will execute while the Promise is waiting to resolve.

The example below demonstrates this behavior in action:

async-function

In this example, the first output is the start. When JavaScript encounters the await keyword, it recognizes that the Promise will take five seconds to resolve. At this point, the function is suspended, and JavaScript moves on to execute any code outside the function. As a result, We are outside is printed next.

Once the Promise is resolved after five seconds, the handlePromise() function is restored to the call stack, and its remaining lines are executed, printing Promise is resolved followed by the end.

let's examine one more example, we'll try making an API call with async/await in other to understand the the concept better.

fetch call
In the code above, the execution process follows the same principles discussed earlier. When JavaScript encounters the fetch function, it suspends the getData() functions and wait for the fetch call to return a response object, this object contains various properties such as the status, headers and body of the response. The function then resumes execution once the response is available.

The response body is the data we need, but it is in raw form (such as text or binary) and not immediately usable. To convert it into a JavaScript object for easier manipulation, we use the .json() method, which parses the raw JSON response. This process involves another Promise, which is why the second await is necessary. The function suspends again until the Promise resolves.

Once both Promises are fulfilled, the getData() function resumes, and the parsed data is printed to the console. A straightforward way to explain how fetch works, isn’t it? Now, back to our main discussion!. What if our API response fails? How do we manage errors with async/await? Let’s dive into this in the next section.

Handling Errors with Async/Await

Traditionally, errors in Promises were handled using the .catch method. But how can we handle errors when using async/await? This is where the try...catch block comes into play.

try-catch
In the code above, the Promise is enclosed within a try block, which executes if the Promise resolves successfully. However, if the Promise is rejected, the error is caught and handled within the catch block.

But did you know we can still handle errors the traditional way? Here’s an example:

catch-method
To handle errors in async/await using the traditional approach, you simply attach the catch method to the function, as demonstrated above. It functions in the same way as the try/catch block.

Conclusion

Async/await has revolutionized how JavaScript handles asynchronous operations, making code more readable and easier to manage compared to traditional methods like .then and .catch. By leveraging async and await, we can write asynchronous code that feels more synchronous, improving overall code clarity. While understanding the inner workings—such as the event loop and microtask queue, implementing async/await is straightforward and highly effective for modern JavaScript development. With proper error handling using try/catch or .catch, we can confidently manage both successful and failed promises.

Thanks for sticking around! I hope this article made async/await a bit clearer for you. Wishing you success in your coding adventures—go build something amazing!

Top comments (0)