Async/Await Concept

Introduction

If you are new to JavaScript, you might have heard terms like callbacks, promises, & async/await. These concepts are fundamental to JavaScript, especially when dealing with tasks like fetching data from an API or performing some mutation.

In web development, working with asynchronous operations is inevitable. Whether you are fetching data from an API or querying a database, handling these asynchronous tasks efficiently is essential. In the past, JavaScript developers had to rely on callbacks, leading to the infamous callback hell. Later promises arrived as a solution, but chaining them could still lead to unwieldy code. Thankfully async/await has emerged as a powerful and intuitive way, making code readable & efficient.

In this blog post, we will explore & focus on how async/await works but first, we need to understand the different types of asynchronous code.

Different types of asynchronous code

Callbacks

Callbacks were the original way to handle asynchronous operations in JavaScript. A callback is a function passed as an argument to another function, to be called back at a later time.

While callbacks work, they can lead to callback hell when dealing with multiple asynchronous operations, making code hard to read & maintain.

Promises

Promises were introduced to address the issues with callbacks. A promise represents a value that may be available now, in the future or never.

Promises improve readability but can still become complex with multiple chained operations.

Async/Await

Async/await is a syntactic sugar built on top of promises. It allows you to write asynchronous code that looks synchronous, making it easier to read & write.

This async/await version looks more linear & easier to follow, especially as the complexity of the asynchronous operations grows.

So which one should you use?

For modern JavaScript development, async/await is generally the preferred method for handling asynchronous code. It combines the advantages of promises with a syntax that is easier to read & write. By making asynchronous code look synchronous, it simplifies the flow and reduces the cognitive load when reading and maintaining code.

Recommendation?

By choosing the right tool for the job and understanding the strengths and weaknesses of each approach, you can write efficient, readable, & maintainable code.

Use async/await for most of your asynchronous code to write clean & maintainable code. This does not mean you should not use callbacks & promises anymore.

Understanding callbacks & promises is very crucial, especially if you want to maintain or work with certain libraries and codebases that rely on them. In some cases, especially when working with older APIs or codebases, callbacks & promises might still be used. Knowing how to work with them ensures compatibility and the ability to refactor or integrate legacy code.

How async/await works?

When you declare a function with the async keyword, it automatically returns a Promise. Inside this function, you can use the await keyword, which pauses the function execution until that Promise settles (either fulfilled or rejected).

The code above demonstrates the basic usage of async & await, but its considered bad practice because it lacks proper error handling. When fetching data from an API, error handling is very crucial due to the unpredictable nature of network requests. Servers can be unstable, APIs might be down or responses might not be in the expected format. Without error handling, your application can encounter runtime errors that are hard to debug.

How can we write better async code?

To make async code better, we should handle potential errors using try...catch blocks. This way, we can catch exceptions thrown during the asynchronous operations and handle them properly.

Why is error handling important?

  • Unstable APIs/Servers: External APIs & servers can be unreliable. Network issues, server downtime, or incorrect endpoints can cause requests to fail.
  • Runtime Errors: Without proper error handling, your application might crash or behave unexpectedly when an error occur.
  • Debugging: Runtime errors without clear handling make debugging difficult. For beginners and junior developers, understanding where and why the code failed becomes a significant challenge.
  • Security Risks: Unhandled errors can expose sensitive information or make your application vulnerable to security risks.

Deep dive into some concepts with async/await

How does event loop work with asynchronous code?

Understanding how asynchronous code interacts with the event loop is crucial, as it helps you troubleshoot errors related to asynchronous operations. I will not dive deeply into this topic since event loop is a broad topic, but here is a simple overview of how asynchronous execution works.

Initiating Asynchronous Operations

When you write asynchronous code using async/await, promises or callbacks you are instructing JavaScript to handle certain operations without blocking the main execution thread. Basically these operations execute in the background.

Role of Event Loop

Event loop manages asynchronous tasks by coordinating between the call stack (where your code runs) and the Queue (where callbacks, promises, & async/await tasks wait to be executed).

GIFGIF created by Sung Jun Eun

If you are curious to dive deeper into the event loop, I highly encourage you to check out Lydia Hallie videos.

JavaScript Visualized - Promise ExecutionJavaScript Visualized - Event Loop

Performance considerations

While async/await simplifies code, its important to be mindful of performance.

Sequential Execution (Not Recommended)

Consider a scenario where you need to fetch data from multiple URLs. A common mistake is to use await inside a loop, which causes each asynchronous operation to wait for the previous one to complete before starting the next. This results in sequential execution, which can be inefficient when the async operations are independent of each other.

Inefficient code

In the code above, each fetch request waits for the previous one to finish. If you have five URLs, the total time will be roughly the sum of the time taken by each request.

Parallel Execution (Recommended)

To execute multiple asynchronous operations in parallel, you can initiate all the promises first and then wait for all of them to resolve using Promise.all. This approach allows the operations to run concurrently, significantly improving performance.

Efficient code

Why does this Matter?

Running asynchronous operations in parallel can drastically reduce the total execution time. Instead of waiting for each operation to complete before starting the next one, you leverage the non-blocking nature of JavaScript to handle tasks concurrently.

My thoughts

Async/await has revolutionized the way we handle asynchronous operations in JavaScript. By writing code that is both readable & maintainable, we can focus more on solving complex problems rather than managing convoluted code structures.

In my own journey, embracing async/await has not only improved the quality of my code but also enhanced my productivity as a developer. I encourage you to dive deep into async/await, experiment with advanced patterns, and see how it can transform your approach.