Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Async/Await in JavaScript: Writing Cleaner Asynchronous Code
Promises fixed callback hell. But chaining many .then() calls still looks messy. Async/await lets you write asynchronous code that reads like normal code.
If promises made async bearable, async/await made it obvious.
Why Async/Await Was Introduced
Promises solved callback hell, but promise chains can be hard to follow.
The Promise Chain Problem
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`/api/posts/${user.id}`)
.then(response => response.json())
.then(posts => ({ user, posts }));
});
}
It works, but reading it requires jumping between .then() blocks.
The Async/Await Solution
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
}
Same logic, reads top to bottom like normal code.
Why It Matters
- Easier to understand
- Variables are in scope where you use them
- Error handling with try/catch instead of
.catch() - Feels like synchronous code
How Async Functions Work
An async function automatically returns a promise.
Basic Example
async function myFunction() {
return "Hello";
}
myFunction().then(result => console.log(result));
// Output: Hello
Even though you return a string, the function returns a promise.
What Gets Returned
async function getNumber() {
return 42;
}
const result = getNumber();
console.log(result); // Promise { <pending> }
result.then(num => console.log(num)); // 42
Every async function returns a promise.
Two Ways to Call Async Functions
Option 1: With .then()
async function getMessage() {
return "Hello!";
}
getMessage().then(msg => console.log(msg));
Option 2: With await (inside another async function)
async function main() {
const msg = await getMessage();
console.log(msg);
}
main();
Simple Async Example
async function processData() {
const response = await fetch("/api/data");
const data = await response.json();
const processed = data.map(item => item * 2);
return processed;
}
processData().then(result => console.log(result));
Each await pauses until the promise resolves.
The Await Keyword
The await keyword pauses execution until a promise resolves.
Basic Example
async function example() {
console.log("Start");
const result = await fetch("/api/data");
console.log("End");
}
// Output:
// Start
// (waits for fetch)
// End
The function pauses at await and waits for the promise.
Multiple Awaits
async function loadUserPosts(userId) {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
}
Each line waits for the previous one to finish.
Await Only Works in Async Functions
// Wrong: This doesn't work
function notAsync() {
const data = await fetch("/api/data"); // SyntaxError
}
// Right: This works
async function isAsync() {
const data = await fetch("/api/data"); // OK
}
You can only use await inside async functions.
Parallel Operations With Promise.all()
If operations don't depend on each other, run them together:
async function loadBoth() {
const [user, posts] = await Promise.all([
fetch("/api/users/1").then(r => r.json()),
fetch("/api/posts/1").then(r => r.json())
]);
return { user, posts };
}
This is faster than waiting for one, then the other.
Error Handling With Async Code
Use try/catch blocks with async/await.
Basic Try/Catch
async function fetchUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user;
} catch (error) {
console.log("Error:", error.message);
}
}
fetchUser(1);
If any await fails, catch handles it.
Error Object Details
async function operation() {
try {
const result = await riskyFunction();
console.log("Success:", result);
} catch (error) {
console.log("Message:", error.message);
console.log("Type:", error.name);
console.log("Stack:", error.stack);
}
}
The error object has all the details.
Finally Block
async function processFile(filename) {
let file;
try {
file = await openFile(filename);
const content = await file.read();
console.log("Content:", content);
} catch (error) {
console.log("Error:", error.message);
} finally {
if (file) {
await file.close(); // Always close
}
}
}
The finally block always runs.
Handle Different Errors
async function complexOp() {
try {
const user = await getUser();
const posts = await getPosts(user.id);
return { user, posts };
} catch (error) {
if (error.message.includes("User not found")) {
console.log("No user");
} else if (error.message.includes("Network")) {
console.log("Network problem");
} else {
console.log("Other error:", error);
}
}
}
Promise vs Async/Await
Both work the same way. Async/await is just cleaner syntax.
Side-by-Side Comparison
With Promises:
function getUser(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
console.log("User:", user);
return user;
})
.catch(error => console.log("Error:", error));
}
With Async/Await:
async function getUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
console.log("User:", user);
return user;
} catch (error) {
console.log("Error:", error);
}
}
Async/await reads more naturally.
Readability Difference
Promises:
.then(...)
.then(...)
.then(...)
.catch(...)
Requires jumping between blocks
Async/Await:
await operation1
await operation2
await operation3
try/catch
Reads top to bottom
They're Compatible
You can use .then() with async/await:
async function loadData() {
const response = await fetch("/api/data");
return response.json();
}
// Still use .then()
loadData().then(data => console.log(data));
// Or await it
const data = await loadData();
Async/await doesn't replace promises. It's built on them.
Converting Promises to Async/Await
Example: Fetch Multiple Resources
Promise Version:
function loadUserWithPosts(userId) {
let userData;
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
userData = user;
return fetch(`/api/posts/${user.id}`);
})
.then(response => response.json())
.then(posts => {
return { user: userData, posts };
})
.catch(error => {
console.log("Error:", error);
return null;
});
}
Async/Await Version:
async function loadUserWithPosts(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.log("Error:", error);
return null;
}
}
Much clearer. No temporary variables or nested callbacks.
Async Function Execution Flow
START
|
v
Enter async function
|
v
Execute line by line
|
├─ await expression → pause
| |
| └─ wait for promise
| |
| └─ resolve
|
├─ Continue
|
└─ await expression → pause
|
└─ wait for promise
|
└─ resolve
|
v
Return value (wrapped in promise)
|
v
END
Each await pauses execution. When the promise resolves, execution continues.
Real-World Example: API Call with Data Processing
async function fetchAndProcessUserData(userId) {
try {
// Fetch user
console.log(`Fetching user ${userId}...`);
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error(`HTTP error! status: ${userResponse.status}`);
}
const user = await userResponse.json();
// Fetch posts
console.log(`Fetching posts for ${user.name}...`);
const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();
// Process data
const result = {
user: user.name,
email: user.email,
postCount: posts.length,
recentPost: posts[0]?.title
};
console.log("Processing complete");
return result;
} catch (error) {
console.error("Failed to fetch data:", error.message);
return null;
} finally {
console.log("Operation finished");
}
}
// Call it
fetchAndProcessUserData(1)
.then(result => console.log("Result:", result))
.catch(err => console.log("Outer error:", err));
Clean, readable, and easy to follow.
Practice Assignment
1. Convert a promise chain to async/await:
// Promise version
function getDataAndProcess() {
return fetch("/api/data")
.then(r => r.json())
.then(data => processData(data))
.catch(err => console.log(err));
}
// Convert to async/await
2. Handle errors with try/catch:
async function fetchMultiple() {
// Fetch from /api/users and /api/posts
// Handle any errors that occur
// Log success and errors
}
3. Use parallel requests:
async function loadDashboard() {
// Fetch users, posts, and comments at the same time
// Use Promise.all() for parallel requests
// Return all data combined
}
4. Chain dependent operations:
async function getUserPostComments(userId) {
// Get user
// Get user's posts
// Get comments on first post
// Return everything together
}
Quick Recap
Async/await is syntactic sugar for promises.
asyncfunctions always return a promise.awaitpauses execution until a promise resolves.awaitonly works insideasyncfunctions.Use
try/catchfor error handling.finallyblock always runs.Async/await is easier to read than promise chains.
Use
Promise.all()to run operations in parallel.Variables are in scope where you use them.
.then()and async/await can be mixed.Async/await code reads like synchronous code.
Async/await makes promises easier to work with. Learn both and you'll truly understand JavaScript's async model.
Happy coding! 🚀
If you enjoyed this article, check out my other blogs on this profile. 🔗 Connect with me: LinkedIn | GitHub | X (Twitter)




