Async / Await
async / await is a modern syntax for writing asynchronous JavaScript that builds directly on top of Promises. It allows asynchronous code to be written in a style that closely resembles synchronous code, improving readability and making control flow easier to reason about.
The primary advantages of async / await are clearer intent, simpler error handling, and the elimination of deeply nested .then() chains that can be difficult to debug and maintain.
Example
Recall our earlier fetch example written using Promises.
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error))
.finally(() => console.log('All done'));
Using async/await, the code can be refactored to look like this:
async function fetchData() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
} finally {
console.log('All done');
}
}
fetchData();
While this version may use a few more lines, it is easier to read because it follows a top-to-bottom, synchronous-looking flow. Additionally, if the functions inside the .then() statements were more complex, readability and debuggability would be even more impacted. The async/await example is far clearer.
Example 2: Sequencing multiple async operations
Let’s say we have three async functions:
async function getUser(id) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
return res.json();
}
async function getPosts(userId) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
return res.json();
}
async function getComments(postId) {
const res = await fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`);
return res.json();
}
We want to:
- Get a user
- Get that user’s posts
- Get comments for the first post
async function loadUserActivity(userId) {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
const latest = posts[0];
const comments = await getComments(latest.id);
return {
user,
latestPost: latest,
comments
};
} catch (err) {
console.error("Error loading activity:", err);
}
}
const activity = await loadUserActivity(1);
console.log(activity);
This shows how async/await handles dependent operations cleanly and clearly.
Example 3: Running async operations in parallel (Promise.all)
When two tasks do not depend on each other, run them together.
Instead of:
const comments = await getComments(postId);
const likes = await getLikes(postId);
…which runs sequentially and takes longer than necessary, use:
const [comments, likes] = await Promise.all([
getComments(postId),
getLikes(postId)
]);
Full example
async function getPostActivity(postId) {
try {
const [comments, likes] = await Promise.all([
getComments(postId),
getLikes(postId)
]);
return { comments, likes };
} catch (err) {
console.error("Error loading post activity:", err);
return null;
}
}
Parallelism is one of the biggest advantages of Promises, and async / await lets you take advantage of it without complex chaining.
Example 4: async/await in loops
async/await does not work the way most people expect with .forEach().
Avoid:
ids.forEach(async id => {
await doSomething(id);
});
.forEach() does not understand async functions and will not wait for them to resolve. This does not wait. The loop finishes immediately.
Correct (sequential):
for (const id of ids) {
await doSomething(id);
}
This ensures each operation finishes before the next begins.
Correct (parallel):
await Promise.all(ids.map(id => doSomething(id)));
This runs all tasks simultaneously — ideal when operations don’t depend on each other.
Example 5: Handling delays (one of the few valid uses of new Promise)
When using async / await, you should rarely need to construct Promises manually.
The one valid exception is creating intentional delays:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
// Usage:
async function demo() {
console.log("Start");
await wait(1000);
console.log("1 second later");
}
This is clean, predictable, and common in real-world async workflows (retry logic, animations, rate limiting, etc.).
Example 6: A complete real-world workflow
This combines everything:
- Sequential steps
- Parallel steps
- Error handling
- Returning structured data
- Modern async patterns
async function getUserDashboard(userId) {
try {
// Step 1: Load user
const user = await getUser(userId);
// Step 2: Load user posts
const posts = await getPosts(user.id);
const latest = posts[0];
// Step 3: Load activity in parallel
const [comments, likes] = await Promise.all([
getComments(latest.id),
getLikes(latest.id)
]);
return {
user,
latestPost: latest,
comments,
likes
};
} catch (err) {
console.error("Error loading dashboard:", err);
return null;
}
}
Usage
const dashboard = await getUserDashboard(1);
console.log(dashboard);
This pattern represents a typical async workflow in real applications: fetch → fetch → parallel fetch → return final result.
Summary
From this point forward, you should be comfortable using:
- ✔ Sequential async calls
- ✔ Parallel async calls with
Promise.all - ✔
try/catcherror handling - ✔ Async loops (
for...ofor Promise.all with map) - ✔ Avoiding
.then()andnew Promise()unless truly necessary
Async/await simplifies complex asynchronous logic and leads to code that is easier to write, debug, and understand.