Async/Await
Async/await is modern JavaScript syntax that makes asynchronous code look and behave more like synchronous code. It's built on top of Promises but provides a cleaner, more readable way to work with asynchronous operations.
Understanding Async Functions
The async keyword is used to declare an asynchronous function. An async function always returns a Promise:
// Regular function
function regularFunction() {
return "Hello";
}
// Async function - automatically wraps return value in a Promise
async function asyncFunction() {
return "Hello";
}
console.log(regularFunction()); // "Hello"
console.log(asyncFunction()); // Promise {<fulfilled>: "Hello"}
// To get the value, use .then() or await
asyncFunction().then(result => console.log(result)); // "Hello"
Key Point: Any function declared with async automatically returns a Promise. If you return a value, it's wrapped in a resolved Promise. If you throw an error, it's wrapped in a rejected Promise.
The Await Keyword
The await keyword can only be used inside async functions. It pauses the execution of the async function and waits for the Promise to resolve:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demo() {
console.log("Starting...");
await delay(2000); // Pause for 2 seconds
console.log("After 2 seconds");
await delay(1000); // Pause for 1 more second
console.log("After 3 seconds total");
}
demo();
// Output:
// Starting... (immediately)
// After 2 seconds (after 2 seconds)
// After 3 seconds total (after 3 seconds)
Error Handling with Try/Catch
With async/await, we can use traditional try/catch blocks for error handling, which is much more intuitive than Promise .catch():
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching user:", error.message);
throw error; // Re-throw if needed
}
}
// Using the function
async function displayUser() {
try {
const user = await fetchUserData(123);
console.log("User:", user);
} catch (error) {
console.log("Failed to load user");
}
}
Best Practice: Always wrap await calls in try/catch blocks to handle potential errors gracefully. This prevents unhandled promise rejections.
Async/Await vs Promises
Here's the same code written with Promises and then with async/await:
// Using Promises (harder to read)
function getUserPosts(userId) {
return fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`https://api.example.com/posts?userId=${user.id}`);
})
.then(response => response.json())
.then(posts => {
return posts;
})
.catch(error => {
console.error("Error:", error);
throw error;
});
}
// Using Async/Await (much cleaner!)
async function getUserPosts(userId) {
try {
const userResponse = await fetch(`https://api.example.com/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`https://api.example.com/posts?userId=${user.id}`);
const posts = await postsResponse.json();
return posts;
} catch (error) {
console.error("Error:", error);
throw error;
}
}
Sequential vs Parallel Execution
Understanding when operations run sequentially vs in parallel is crucial for performance:
// SEQUENTIAL (slower - waits for each request)
async function getDataSequential() {
const user = await fetch("/api/user");
const posts = await fetch("/api/posts");
const comments = await fetch("/api/comments");
return {
user: await user.json(),
posts: await posts.json(),
comments: await comments.json()
};
}
// Total time: time1 + time2 + time3
// PARALLEL (faster - all requests start immediately)
async function getDataParallel() {
// Start all requests at once
const [userRes, postsRes, commentsRes] = await Promise.all([
fetch("/api/user"),
fetch("/api/posts"),
fetch("/api/comments")
]);
return {
user: await userRes.json(),
posts: await postsRes.json(),
comments: await commentsRes.json()
};
}
// Total time: max(time1, time2, time3)
Performance Tip: Only use sequential await when one operation depends on the result of another. Otherwise, use Promise.all() to run operations in parallel for better performance.
Common Patterns and Best Practices
// ✓ GOOD: Parallel independent operations
async function loadDashboard() {
const [user, stats, notifications] = await Promise.all([
fetchUser(),
fetchStats(),
fetchNotifications()
]);
return { user, stats, notifications };
}
// ✗ BAD: Sequential independent operations
async function loadDashboardSlow() {
const user = await fetchUser();
const stats = await fetchStats(); // Waits unnecessarily
const notifications = await fetchNotifications(); // Waits unnecessarily
return { user, stats, notifications };
}
// ✓ GOOD: Sequential dependent operations
async function createUserAndProfile(userData) {
const user = await createUser(userData);
const profile = await createProfile(user.id); // Needs user.id
return { user, profile };
}
// ✓ GOOD: Error handling with specific messages
async function processPayment(orderId) {
try {
const order = await fetchOrder(orderId);
const payment = await chargeCard(order.total);
await sendConfirmation(order.email);
return payment;
} catch (error) {
if (error.message.includes("card")) {
throw new Error("Payment failed. Please check your card details.");
}
throw new Error("Order processing failed. Please try again.");
}
}
Async/Await with Array Methods
Be careful when using async/await with array methods like map() and forEach():
// ✗ WRONG: forEach doesn't wait for async operations
async function processUsersWrong(userIds) {
userIds.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user);
}); // This completes immediately without waiting
}
// ✓ CORRECT: Use for...of loop
async function processUsersCorrect(userIds) {
for (const id of userIds) {
const user = await fetchUser(id);
console.log(user);
}
}
// ✓ CORRECT: Use Promise.all with map for parallel processing
async function processUsersParallel(userIds) {
const users = await Promise.all(
userIds.map(id => fetchUser(id))
);
users.forEach(user => console.log(user));
}
Real-World Example: User Registration Flow
async function registerUser(formData) {
try {
// 1. Validate email is not taken
const emailExists = await checkEmailExists(formData.email);
if (emailExists) {
throw new Error("Email already registered");
}
// 2. Create user account
const user = await createUserAccount(formData);
// 3. These can run in parallel
await Promise.all([
sendWelcomeEmail(user.email),
createUserProfile(user.id),
logRegistrationEvent(user.id)
]);
// 4. Generate session token
const token = await generateAuthToken(user.id);
return { user, token };
} catch (error) {
console.error("Registration failed:", error.message);
throw error;
}
}
// Usage
async function handleRegistration(formData) {
try {
const result = await registerUser(formData);
console.log("Registration successful!");
return result;
} catch (error) {
alert(`Registration failed: ${error.message}`);
}
}
Practice Exercise:
Task: Create an async function that fetches a user and their posts, then adds comment counts to each post.
// Your task: Implement this function
async function getUserWithPostStats(userId) {
// 1. Fetch user data
// 2. Fetch user's posts
// 3. For each post, fetch comment count
// 4. Return user with posts including comment counts
}
// Expected output structure:
// {
// user: { id: 1, name: "John" },
// posts: [
// { id: 1, title: "Post 1", commentCount: 5 },
// { id: 2, title: "Post 2", commentCount: 3 }
// ]
// }
Solution:
async function getUserWithPostStats(userId) {
try {
// Fetch user and posts in parallel
const [userRes, postsRes] = await Promise.all([
fetch(`https://api.example.com/users/${userId}`),
fetch(`https://api.example.com/posts?userId=${userId}`)
]);
const user = await userRes.json();
const posts = await postsRes.json();
// Fetch comment counts for all posts in parallel
const postsWithComments = await Promise.all(
posts.map(async (post) => {
const commentsRes = await fetch(
`https://api.example.com/comments?postId=${post.id}`
);
const comments = await commentsRes.json();
return {
...post,
commentCount: comments.length
};
})
);
return { user, posts: postsWithComments };
} catch (error) {
console.error("Error:", error);
throw error;
}
}
Summary
In this lesson, you learned:
- Async functions automatically return Promises
- Await pauses execution until a Promise resolves
- Try/catch blocks handle errors in async functions
- Async/await provides cleaner syntax than Promise chains
- Use Promise.all() for parallel operations
- Sequential await should only be used for dependent operations
- Be careful with async operations in array methods
Next Up: In the next lesson, we'll explore the Fetch API and learn how to make HTTP requests using async/await!