Async/Await in JavaScript: Writing Cleaner Asynchronous Code
Making Asynchronous Code Feel Synchronous (Without Breaking JavaScript)
Introduction
If you've worked with asynchronous JavaScript, you've probably seen .then() chains from Promises.
They work but after a point, they start to feel messy and hard to read.
That's exactly why async/await was introduced.
It helps you write asynchronous code in a way that looks and feels more like normal, synchronous code.
Why Async/Await Was Introduced
Before async/await we had,
Callbacks --> messy nesting (callback hell)
Promises --> better, but still chained and less readable i complex flows.
Example with Promises:
fetchData()
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log(err));
This is fine for small cases. But as logic grows, chaining becomes harder to manage.
Async/await solves this by flattening the structure.
How Async Functions Work
An async function always return a Promise.
async function greet() {
return "Hello";
}
Even though it returns a sting, JavaScript wraps it in a Promise:
greet().then(console.log); // Hello
So internally,
async= function that returns a PromiseYou don't need to manually create one
The Await Keyword (Core Concept)
await pauses the execution of an async function until a Promise resolves.
async function getData() {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
}
What is happening here,
JavaScript waits for
fetch()to completeThen waits for
.json()to completeThem moves forward
It looks synchronous, but it’s still asynchronous under the hood.
Comparing with Promises
function getData() {
fetch("https://api.example.com/data")
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log(err));
}
Now with async/await:
async function getData() {
try {
const res = await fetch("https://api.example.com/data");
const data = await res.json();
console.log(data);
} catch (err) {
console.log(err);
}
}
Difference:
Promises → chaining
Async/await → step-by-step flow
Much easier to read and debug.
Error Handling with Async Code
With async/await, we use try...catch
async function getUser() {
try {
const res = await fetch("wrong-url");
const data = await res.json();
console.log(data);
} catch (error) {
console.log("Something went wrong:", error);
}
}
This feels like normal error handling in synchronous code.
A Simple Real-Life Example
Let’s simulate an API call:
function fetchUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("User Data");
}, 2000);
});
}
Using Promises:
fetchUser().then(data => console.log(data));
Using async/await:
async function showUser() {
const data = await fetchUser();
console.log(data);
}
showUser();
Cleaner. Easier to follow. Less mental load.
Why Async/Await Feels Better
Code looks synchronous
Easier to read
Easier to debug
Works well with
try...catchReduces nested logic
Important:
Async/await is just syntactic sugar over Promises — it doesn’t replace them.
Small Edge Case to Know
If you use multiple awaits one after another, they run sequentially, not in parallel.
const a = await task1();
const b = await task2();
This can slow things down.
Better approach when tasks are independent:
const [a, b] = await Promise.all([task1(), task2()]);
Conclusion
Async/await doesn’t change how JavaScript works internally — it just makes your code cleaner and easier to understand.
If Promises were about control,
async/await is about clarity.