Async/Await in JavaScript: Writing Cleaner Asynchronous Code

How async/await improves readability compared to callbacks and promises.
JavaScript applications often need to perform tasks that take time to complete. For example, fetching data from an API, reading files, or waiting for timers are all operations that do not finish instantly. These types of tasks are known as asynchronous operations.
Earlier, JavaScript developers used callbacks to handle asynchronous code. Later, Promises were introduced to improve the readability and structure of asynchronous programming. However, promise chains can still become complex when many asynchronous steps are involved.
To make asynchronous code even easier to read and write, JavaScript introduced Async/Await.
Async/Await allows developers to write asynchronous code in a way that looks almost like normal synchronous code. This makes programs easier to understand and maintain.
In this article, we will explore:
Why async/await was introduced
How async functions work
The concept of the await keyword
Error handling in async code
Comparison between promises and async/await
By the end of this article, you will understand how async/await simplifies asynchronous JavaScript programming.
Why Async/Await Was Introduced
Before async/await existed, developers used callbacks to handle asynchronous tasks. While callbacks work well for simple operations, they often create deeply nested code when multiple tasks depend on each other.
Example of Callback Nesting
setTimeout(() => {
console.log("Step 1 completed");
setTimeout(() => {
console.log("Step 2 completed");
setTimeout(() => {
console.log("Step 3 completed");
}, 1000);
}, 1000);
}, 1000);
This structure becomes hard to read as more tasks are added.
Promises improved the situation by allowing developers to chain asynchronous operations.
Example with Promises
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.log(error);
});
This approach is cleaner than callbacks, but long promise chains can still be difficult to follow.
Async/Await was introduced to make asynchronous code look more like synchronous code, improving readability and reducing complexity.
What is Async/Await?
Async/Await is a modern JavaScript feature used to handle asynchronous operations in a cleaner and more readable way.
It is built on top of Promises and works by using two keywords:
asyncawait
These keywords allow asynchronous code to be written in a style that looks similar to regular synchronous code.
How Async Functions Work
An async function is a function declared using the async keyword. When a function is marked as async, it always returns a promise.
Example
async function greet() {
return "Hello World";
}
greet().then(message => console.log(message));
Output
Hello World
Even though the function returns a string, JavaScript automatically wraps the return value inside a promise.
This means async functions always behave like promises.
Using Await Inside Async Functions
The await keyword is used inside an async function to pause the execution of the function until a promise is resolved.
This allows asynchronous code to appear more like normal step-by-step code.
Example
function waitTwoSeconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve("Finished waiting");
}, 2000);
});
}
async function runTask() {
const result = await waitTwoSeconds();
console.log(result);
}
runTask();
Output
Finished waiting
Here is what happens:
waitTwoSeconds()returns a promise.The
awaitkeyword pauses execution until the promise resolves.Once resolved, the result is stored in the variable
result.
This makes the code look very similar to synchronous programming.
Async Function Execution Flow
Async Function Starts
|
v
Encounter await
|
v
Wait for Promise
|
v
Promise Resolved
|
v
Continue Execution
This flow makes asynchronous code easier to understand.
Example: Fetching API Data with Async/Await
Let’s see a practical example using the fetch API.
Promise Version
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log(error));
Async/Await Version
async function getPost() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await response.json();
console.log(data);
} catch (error) {
console.log(error);
}
}
getPost();
The async/await version looks much cleaner and easier to follow.
Error Handling with Async/Await
Handling errors is an important part of asynchronous programming. With promises, errors are handled using .catch().
With async/await, we can use try...catch blocks.
Example
async function getUser() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
const data = await response.json();
console.log(data);
} catch (error) {
console.log("Error occurred:", error);
}
}
getUser();
If the request fails, the error will be caught in the catch block.
Using try/catch makes error handling easier and more readable.
Async/Await as Syntactic Sugar
Async/Await is often described as syntactic sugar for promises.
This means it does not replace promises internally. Instead, it provides a simpler syntax to work with them.
Under the hood, async/await still uses promises.
Example:
async function example() {
return "Hello";
}
Is similar to:
function example() {
return Promise.resolve("Hello");
}
Async/await simply hides the complexity of promises and makes the code easier to read.
Promise vs Async/Await Comparison
Promise Style
function getData() {
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log(error));
}
Async/Await Style
async function getData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await response.json();
console.log(data);
} catch (error) {
console.log(error);
}
}
Async/Await improves readability by making asynchronous code look sequential.
Promise vs Async/Await Flow Diagram
Promise Flow
Start
|
v
Promise Created
|
v
.then()
|
v
.then()
|
v
.catch()
Async/Await Flow
Start
|
v
Async Function
|
v
await Promise
|
v
Result Returned
|
v
Continue Execution
The async/await version is much easier to follow.
Benefits of Async/Await
Async/Await provides several advantages for developers.
Improved Readability
Code written with async/await looks more like normal sequential code.
Better Error Handling
Using try/catch blocks makes error handling clearer.
Cleaner Code
Async/await reduces the need for long .then() chains.
Easier Debugging
Debugging async/await code is often simpler than debugging promise chains.
Important Rules of Async/Await
There are a few important rules when using async/await.
awaitcan only be used inside an async function.Async functions always return a promise.
Await pauses execution until the promise resolves.
Errors should be handled using try/catch.
Real-World Uses of Async/Await
Async/Await is widely used in modern JavaScript applications.
Common uses include:
Fetching API data
Database queries
File operations
Network requests
Server-side development with Node.js
Most modern frameworks such as React, Next.js, and Node.js use async/await extensively.
Conclusion
Async/Await is one of the most important features in modern JavaScript. It simplifies asynchronous programming by making code easier to read and write.
In this article, we explored:
Why async/await was introduced
How async functions work
The purpose of the await keyword
Handling errors using try/catch
The relationship between promises and async/await
Async/Await allows developers to write asynchronous code that looks like synchronous code while still benefiting from the power of promises.
Understanding async/await is essential for building modern JavaScript applications, as it helps developers write cleaner, more readable, and maintainable asynchronous code.
