How to Use Async/Await in JavaScript: A Modern Approach to Asynchronous Functions
In this article, we will explore the modern approach to handling asynchronous functions in JavaScript using async/await syntax. JavaScript has evolved rapidly, transitioning from callbacks to promises (ES2015), and now providing even simpler asynchronous JavaScript with the introduction of async/await in ES2017.
Introduction
Async functions in JavaScript combine the features of promises and generators, serving as a higher-level abstraction over promises. It is important to note that async/await is built on promises.
Why were async/await introduced?
Async/await was introduced to reduce the boilerplate code associated with promises, as well as to address the limitations of chaining promises known as the “don’t break the chain” problem.
While promises were introduced in ES2015 to solve the issue of “callback hell”, over the course of the following two years, it became clear that promises alone were not the optimal solution. Although promises provided a much-needed improvement, they introduced their own complexity and syntax intricacies. Thus, async functions were introduced to serve as a more developer-friendly syntax for handling asynchronous code.
Async/await syntax makes asynchronous code appear synchronous and non-blocking, enhancing code readability and maintainability.
How it works
An async function returns a promise. For example:
1 | const doSomethingAsync = () => { |
To call this function, prepend await
, and the calling code will pause until the promise is resolved or rejected. However, the client function must be defined with the async
keyword. Here’s an example:
1 | const doSomething = async () => { |
A quick example
Let’s take a simple example of async/await in action:
1 | const doSomethingAsync = () => { |
The above code will output the following to the browser console:
1 | Before |
Promise all the things
By prefixing the async
keyword to any function, the function will return a promise. Even if the function does not explicitly use a return
statement, it will internally make it return a promise. This allows for shorter and more readable code. For example:
1 | const aFunction = async () => { |
The above code is equivalent to:
1 | const aFunction = async () => { |
The code is much simpler to read
As the examples above illustrate, code written using async/await syntax is significantly more readable and simpler compared to code using plain promises with chained callbacks. This advantage becomes even more pronounced with complex code.
For instance, consider fetching a JSON resource and parsing it using promises:
1 | const getFirstUserData = () => { |
The same functionality can be achieved with much cleaner code using async/await:
1 | const getFirstUserData = async () => { |
Multiple async functions in series
Chaining async functions is straightforward and much more readable than chaining promises with plain syntax:
1 | const promiseToDoSomething = () => { |
The output will be:
1 | I did something and I watched and I watched as well |
Easier debugging
Debugging promises can be challenging, as the debugger does not step over asynchronous code. However, async/await makes debugging much easier since the code appears synchronous to the compiler.