Advanced JavaScript (ES6+)

Async/Await

13 min Lesson 16 of 40

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!