Async/Await Patterns

Advanced async/await usage, error handling, parallel execution, and best practices

Async/Await Fundamentals

async/await is syntactic sugar over Promises that makes asynchronous code look and behave more like synchronous code. It's the preferred way to write async JavaScript in modern applications.

Key Points

  • async — Declares a function that returns a Promise
  • await — Pauses execution until Promise resolves
  • Return value — Automatically wrapped in Promise.resolve()
  • Errors — Thrown errors become rejected promises

Basic Syntax

// Async function declaration
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// Arrow function
const fetchUser = async (id) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
};

// Using the function
fetchUser(1)
  .then(user => console.log(user))
  .catch(error => console.error(error));

// Or with await (inside another async function)
const user = await fetchUser(1);

Error Handling with try/catch

async function fetchData() {
  try {
    const response = await fetch("/api/data");
    
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
    
  } catch (error) {
    console.error("Fetch failed:", error.message);
    throw error; // Re-throw if needed
  } finally {
    console.log("Cleanup code runs regardless");
  }
}

// Multiple operations in one try block
async function processUser(id) {
  try {
    const user = await fetchUser(id);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return { user, posts, comments };
  } catch (error) {
    // Catches errors from any of the three fetches
    console.error("Error in pipeline:", error);
    return null;
  }
}

Parallel Execution

Await multiple promises simultaneously for better performance:

// ❌ Sequential (slow) - each waits for the previous
async function sequential() {
  const users = await fetchUsers();    // Wait 1s
  const posts = await fetchPosts();    // Wait 1s
  const comments = await fetchComments(); // Wait 1s
  // Total: ~3s
}

// ✅ Parallel (fast) - all run at the same time
async function parallel() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),     // Start immediately
    fetchPosts(),     // Start immediately
    fetchComments()   // Start immediately
  ]);
  // Total: ~1s (slowest request)
}

// ✅ Parallel with error handling for each
async function parallelWithFallbacks() {
  const results = await Promise.allSettled([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  
  return results.map(r => 
    r.status === "fulfilled" ? r.value : null
  );
}

Sequential vs Parallel Decision

// Use SEQUENTIAL when operations depend on each other
async function dependent() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(user.id);      // Needs user.id
  const comments = await fetchComments(posts);  // Needs posts
  return { user, posts, comments };
}

// Use PARALLEL when operations are independent
async function independent() {
  const [user, settings, notifications] = await Promise.all([
    fetchUser(),        // Independent
    fetchSettings(),    // Independent
    fetchNotifications() // Independent
  ]);
  return { user, settings, notifications };
}

// HYBRID: Mix both approaches
async function hybrid(userId) {
  const user = await fetchUser(userId);
  
  // These depend on user but not on each other
  const [posts, followers, settings] = await Promise.all([
    fetchPosts(user.id),
    fetchFollowers(user.id),
    fetchSettings(user.id)
  ]);
  
  return { user, posts, followers, settings };
}

Loops with Async/Await

// Sequential loop - one at a time
async function processSequentially(ids) {
  const results = [];
  
  for (const id of ids) {
    const result = await processItem(id);
    results.push(result);
  }
  
  return results;
}

// Parallel loop - all at once
async function processInParallel(ids) {
  const promises = ids.map(id => processItem(id));
  return Promise.all(promises);
}

// Controlled concurrency - batch processing
async function processBatches(ids, batchSize = 5) {
  const results = [];
  
  for (let i = 0; i < ids.length; i += batchSize) {
    const batch = ids.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(id => processItem(id))
    );
    results.push(...batchResults);
  }
  
  return results;
}

// ⚠️ forEach doesn't work with await
// ❌ BAD - won't wait for promises
ids.forEach(async (id) => {
  await processItem(id); // Doesn't wait!
});

// ✅ GOOD - use for...of
for (const id of ids) {
  await processItem(id);
}

Advanced Patterns

// Retry pattern
async function fetchWithRetry(url, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      
      const delay = Math.pow(2, attempt) * 1000;
      console.log(`Retry ${attempt} in ${delay}ms`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

// Timeout pattern
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, { signal: controller.signal });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

// Cancellable async operation
function createCancellableRequest(url) {
  const controller = new AbortController();
  
  const promise = fetch(url, { signal: controller.signal });
  
  return {
    promise,
    cancel: () => controller.abort()
  };
}

const { promise, cancel } = createCancellableRequest("/api/data");
// Later: cancel();

Async in Class Methods

class UserService {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }
  
  async getUser(id) {
    const response = await fetch(`${this.baseUrl}/users/${id}`);
    return response.json();
  }
  
  async createUser(userData) {
    const response = await fetch(`${this.baseUrl}/users`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(userData)
    });
    return response.json();
  }
  
  // Getter can't be async, but can return a Promise
  get currentUser() {
    return this.getUser("me");
  }
}

const service = new UserService("/api");
const user = await service.getUser(1);

⚠️ Common Mistakes

  • Sequential when parallel is possible: Don't await unnecessarily in sequence
  • Using forEach with async: Use for...of or Promise.all() instead
  • Not handling errors: Always use try/catch or .catch()
  • Forgetting await: Results in working with Promise objects, not values
  • await in non-async function: Only works inside async functions

💡 Key Takeaways

  • • async/await makes async code readable and maintainable
  • • Use try/catch for error handling
  • • Use Promise.all() for independent parallel operations
  • • Use for...of for sequential async loops
  • • Consider AbortController for cancellation and timeouts
  • • Always handle errors to prevent unhandled rejections