Async/await provides a cleaner syntax for working with Promises, making asynchronous code look synchronous and dramatically improving readability for sequential operations. It’s not a replacement for Promises — it’s built on top of them. Every async function returns a Promise, and every await expression unwraps one.
Understanding why async/await is an improvement requires seeing both forms side by side. Promises with .then() chaining work well for simple cases but become hard to follow when you need conditional branching, loops, or sharing intermediate values across multiple steps. Async/await handles all of these naturally with the same control flow constructs you already know.
What is Async/Await?
Async/await is syntactic sugar built on top of Promises:
// Promise-based
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(response => response.json())
.then(user => user);
}
// Async/await
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
Async Functions
An async function always returns a Promise:
async function greet(name) {
return `Hello, ${name}!`;
}
greet("Alice").then(message => console.log(message)); // "Hello, Alice!"
Implicit Promise Wrapping
async function test() {
return 42;
}
// Equivalent to
function test() {
return Promise.resolve(42);
}
test().then(value => console.log(value)); // 42
Await Expression
The await keyword pauses execution until a Promise settles:
async function example() {
console.log("Start");
const result = await Promise.resolve("Done");
console.log(result); // "Done"
console.log("End");
}
example();
// Output:
// Start
// Done
// End
Await with Delays
async function delayedGreeting() {
console.log("Waiting...");
await new Promise(resolve => setTimeout(resolve, 2000));
console.log("Hello after 2 seconds!");
}
delayedGreeting();
Error Handling
Try-Catch
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("Error fetching user:", error);
throw error;
}
}
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error(error));
Multiple Try-Catch Blocks
async function processData() {
try {
const data = await fetchData();
console.log("Data fetched");
} catch (error) {
console.error("Fetch error:", error);
}
try {
const processed = await processData(data);
console.log("Data processed");
} catch (error) {
console.error("Processing error:", error);
}
}
Finally Block
async function operation() {
try {
const result = await someAsyncOperation();
return result;
} catch (error) {
console.error(error);
} finally {
console.log("Cleanup"); // Always runs
}
}
Sequential vs Parallel Execution
Sequential (Slower)
async function sequential() {
const user = await fetchUser(1);
const orders = await fetchOrders(user.id);
const details = await fetchOrderDetails(orders[0].id);
return details;
}
// Takes: time1 + time2 + time3
Parallel (Faster)
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
return { user, posts, comments };
}
// Takes: max(time1, time2, time3)
Practical Examples
Fetch with Retry
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Retry ${i + 1}...`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
fetchWithRetry("https://api.example.com/data")
.then(data => console.log(data))
.catch(error => console.error(error));
Timeout
async function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
return Promise.race([promise, timeout]);
}
async function fetchWithTimeout() {
try {
const data = await withTimeout(
fetch("https://api.example.com/data").then(r => r.json()),
5000
);
console.log(data);
} catch (error) {
console.error(error);
}
}
Sequential Processing
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
async function processItem(item) {
return new Promise(resolve => {
setTimeout(() => resolve(`Processed: ${item}`), 1000);
});
}
processItems(["A", "B", "C"])
.then(results => console.log(results));
Parallel Processing
async function processItemsParallel(items) {
const promises = items.map(item => processItem(item));
return Promise.all(promises);
}
processItemsParallel(["A", "B", "C"])
.then(results => console.log(results));
Data Transformation Pipeline
async function getUserWithDetails(userId) {
try {
// Fetch user
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
// Fetch user's posts
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const posts = await postsResponse.json();
// Fetch user's comments
const commentsResponse = await fetch(`/api/users/${userId}/comments`);
const comments = await commentsResponse.json();
return {
user,
posts,
comments,
postCount: posts.length,
commentCount: comments.length
};
} catch (error) {
console.error("Error fetching user details:", error);
throw error;
}
}
getUserWithDetails(1)
.then(details => console.log(details))
.catch(error => console.error(error));
Event Handler with Async
const button = document.getElementById("myButton");
button.addEventListener("click", async (event) => {
try {
button.disabled = true;
button.textContent = "Loading...";
const data = await fetch("/api/data").then(r => r.json());
console.log(data);
button.textContent = "Success!";
} catch (error) {
console.error(error);
button.textContent = "Error";
} finally {
button.disabled = false;
}
});
Async Arrow Functions
// Regular async function
async function fetchData() {
return await fetch("/api/data").then(r => r.json());
}
// Async arrow function
const fetchData = async () => {
return await fetch("/api/data").then(r => r.json());
};
// Concise
const fetchData = async () => fetch("/api/data").then(r => r.json());
Async Generators
async function* asyncGenerator() {
yield 1;
await new Promise(resolve => setTimeout(resolve, 1000));
yield 2;
await new Promise(resolve => setTimeout(resolve, 1000));
yield 3;
}
(async () => {
for await (const value of asyncGenerator()) {
console.log(value);
}
})();
Common Patterns
Debounce with Async
function debounceAsync(fn, delay) {
let timeoutId;
return async (...args) => {
clearTimeout(timeoutId);
return new Promise(resolve => {
timeoutId = setTimeout(() => {
resolve(fn(...args));
}, delay);
});
};
}
const search = debounceAsync(async (query) => {
const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
return results;
}, 300);
Memoization with Async
function memoizeAsync(fn) {
const cache = new Map();
return async (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = await fn(...args);
cache.set(key, result);
return result;
};
}
const fetchUser = memoizeAsync(async (id) => {
return fetch(`/api/users/${id}`).then(r => r.json());
});
Best Practices
Use Parallel When Possible
// Bad - sequential
const user = await fetchUser(1);
const posts = await fetchPosts(1);
// Good - parallel
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(1)
]);
Handle Errors Properly
// Good - specific error handling
async function operation() {
try {
const result = await riskyOperation();
return result;
} catch (error) {
if (error instanceof NetworkError) {
console.error("Network error:", error);
} else if (error instanceof ValidationError) {
console.error("Validation error:", error);
} else {
console.error("Unknown error:", error);
}
throw error;
}
}
Avoid Unnecessary Await
// Bad - unnecessary await
async function bad() {
const result = await Promise.resolve(42);
return result;
}
// Good - return promise directly
async function good() {
return Promise.resolve(42);
}
Summary
- async: declares async function that returns Promise
- await: pauses execution until Promise settles
- try-catch: error handling in async functions
- Sequential: await operations one after another
- Parallel: use Promise.all() for concurrent operations
- Error handling: use try-catch-finally
- Best practice: use parallel execution when possible
Related Resources
Official Documentation
Next Steps
- Fetch API: Making HTTP Requests
- Error Handling with Promises and Async/Await
- DOM: Document Object Model Basics
Comments