Lesson 12: Asynchronous Programming - Promises and Async/Await

What You'll Learn

  • What synchronous vs asynchronous means
  • Understanding Promises
  • Using async/await
  • Handling async errors
  • Promise methods (all, race, etc.)
  • Real-world async patterns

Why This Matters

Many operations take time - fetching data from APIs, reading files, database queries. Asynchronous programming lets your program continue working while waiting for these operations, making your applications faster and more responsive.

---

Part 1: Synchronous vs Asynchronous

Create a new file: lesson-12/async-programming.js

Synchronous Code (Blocking)

console.log("Start");

// This blocks - nothing else happens until it's done
function slowOperation() {
  const end = Date.now() + 2000;  // Wait 2 seconds
  while (Date.now() < end) {
    // Blocking wait
  }
  console.log("Slow operation done");
}

slowOperation();  // Program freezes here for 2 seconds
console.log("End");

// Output:
// Start
// (2 second pause)
// Slow operation done
// End
Problem: Nothing else can happen while waiting!

Asynchronous Code (Non-Blocking)

console.log("Start");

// This doesn't block - program continues
setTimeout(() => {
  console.log("Async operation done");
}, 2000);

console.log("End");

// Output:
// Start
// End
// (2 second pause)
// Async operation done
Better: Program continues while waiting!

---

Part 2: Understanding Promises

A Promise represents a value that will be available in the future.

Promise States

A Promise can be in one of three states:

  1. Pending: Operation hasn't finished yet
  2. Fulfilled: Operation succeeded (resolved)
  3. Rejected: Operation failed (rejected)

Creating a Promise

const myPromise = new Promise((resolve, reject) => {
  // Simulate async operation
  setTimeout(() => {
    const success = true;
    
    if (success) {
      resolve("Operation successful!");  // Fulfill the promise
    } else {
      reject("Operation failed!");       // Reject the promise
    }
  }, 2000);
});

console.log(myPromise);  // Promise { <pending> }

Using a Promise with .then() and .catch()

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data loaded!");
  }, 1000);
});

myPromise
  .then((result) => {
    console.log("Success:", result);
  })
  .catch((error) => {
    console.log("Error:", error);
  });

// Output after 1 second:
// Success: Data loaded!

Promise Chain

function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: "Alice" });
    }, 1000);
  });
}

function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(["Post 1", "Post 2", "Post 3"]);
    }, 1000);
  });
}

fetchUser()
  .then((user) => {
    console.log("User:", user.name);
    return fetchPosts(user.id);  // Return next promise
  })
  .then((posts) => {
    console.log("Posts:", posts);
  })
  .catch((error) => {
    console.log("Error:", error);
  });

// Output:
// User: Alice (after 1 second)
// Posts: ["Post 1", "Post 2", "Post 3"] (after 2 seconds total)

---

Part 3: Async/Await (Modern Way)

async/await makes asynchronous code look and behave more like synchronous code.

Basic async/await

// Function that returns a Promise
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data received!");
    }, 1000);
  });
}

// Old way (with .then())
function getData() {
  fetchData()
    .then((data) => {
      console.log(data);
    });
}

// New way (with async/await)
async function getData2() {
  const data = await fetchData();  // Wait for promise to resolve
  console.log(data);
}

getData2();

Why async/await is Better

// With .then() (harder to read)
fetchUser()
  .then((user) => {
    return fetchPosts(user.id);
  })
  .then((posts) => {
    return fetchComments(posts[0].id);
  })
  .then((comments) => {
    console.log(comments);
  })
  .catch((error) => {
    console.log(error);
  });

// With async/await (easier to read)
async function loadData() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    console.log(comments);
  } catch (error) {
    console.log(error);
  }
}

Rules of async/await

  1. await only works inside async functions
  2. async functions always return a Promise
  3. Use try/catch for error handling
// ❌ WRONG - await outside async function
function wrong() {
  const data = await fetchData();  // Error!
}

// ✅ CORRECT - await inside async function
async function correct() {
  const data = await fetchData();
  console.log(data);
}

// Async function returns a Promise
async function getNumber() {
  return 42;  // Automatically wrapped in Promise
}

getNumber().then((num) => console.log(num));  // 42

---

Part 4: Error Handling with async/await

Using try/catch

async function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id: 1, name: "Alice" });
      } else {
        reject(new Error("User not found"));
      }
    }, 1000);
  });
}

async function getUser(id) {
  try {
    console.log("Fetching user...");
    const user = await fetchUser(id);
    console.log("User found:", user.name);
  } catch (error) {
    if (error instanceof Error) {
      console.log("Error:", error.message);
    }
  }
}

await getUser(1);  // User found: Alice
await getUser(999);  // Error: User not found

Multiple awaits with Error Handling

async function loadUserData(userId) {
  try {
    const user = await fetchUser(userId);
    console.log("User:", user);
    
    const posts = await fetchPosts(userId);
    console.log("Posts:", posts);
    
    const friends = await fetchFriends(userId);
    console.log("Friends:", friends);
  } catch (error) {
    console.log("Failed to load data:", error);
    // All errors caught in one place
  }
}

---

Part 5: Parallel vs Sequential

Sequential (One After Another)

async function sequential() {
  console.time("Sequential");
  
  const user = await fetchUser();      // Wait 1 second
  const posts = await fetchPosts();    // Wait 1 second
  const comments = await fetchComments();  // Wait 1 second
  
  console.timeEnd("Sequential");  // ~3 seconds total
}

Parallel (All at Once)

async function parallel() {
  console.time("Parallel");
  
  // Start all promises at the same time
  const userPromise = fetchUser();
  const postsPromise = fetchPosts();
  const commentsPromise = fetchComments();
  
  // Wait for all to complete
  const user = await userPromise;
  const posts = await postsPromise;
  const comments = await commentsPromise;
  
  console.timeEnd("Parallel");  // ~1 second total
}

Using Promise.all()

async function parallelWithAll() {
  console.time("Promise.all");
  
  // Wait for all promises to complete
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  
  console.log(user, posts, comments);
  console.timeEnd("Promise.all");  // ~1 second total
}
When to use what:
  • Sequential: When next operation depends on previous result
  • Parallel: When operations are independent

---

Part 6: Promise Methods

Promise.all() - All or Nothing

Waits for ALL promises, fails if ANY fails:

async function loadMultiple() {
  try {
    const results = await Promise.all([
      fetchData("user"),
      fetchData("posts"),
      fetchData("comments")
    ]);
    
    console.log("All loaded:", results);
  } catch (error) {
    console.log("One or more failed:", error);
    // If ANY promise fails, entire operation fails
  }
}

Promise.allSettled() - Wait for All (Success or Fail)

Waits for ALL, doesn't care if some fail:

async function loadMultipleSafe() {
  const results = await Promise.allSettled([
    fetchData("user"),
    fetchData("posts"),
    fetchData("comments")
  ]);
  
  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(`${index} succeeded:`, result.value);
    } else {
      console.log(`${index} failed:`, result.reason);
    }
  });
}

Promise.race() - First One Wins

Returns first promise that completes:

async function raceExample() {
  const result = await Promise.race([
    fetchData("slow"),   // Takes 5 seconds
    fetchData("fast"),   // Takes 1 second
    fetchData("medium")  // Takes 3 seconds
  ]);
  
  console.log("Winner:", result);  // Result from "fast"
}
Use case: Timeout pattern:
function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("Timeout")), ms);
  });
}

async function fetchWithTimeout() {
  try {
    const result = await Promise.race([
      fetchData("api"),
      timeout(5000)  // Fail after 5 seconds
    ]);
    console.log("Data:", result);
  } catch (error) {
    console.log("Request timed out");
  }
}

Promise.any() - First Success Wins

Returns first promise that succeeds:

async function anyExample() {
  try {
    // Try multiple servers
    const result = await Promise.any([
      fetchFrom("server1.com"),  // Might fail
      fetchFrom("server2.com"),  // Might fail
      fetchFrom("server3.com")   // Might fail
    ]);
    
    console.log("Got data from one server:", result);
  } catch (error) {
    console.log("All servers failed");
  }
}

---

Part 7: Practical Examples

Example 1: API Fetch Simulation

// Simulate API call
function fetchUsers() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: "Alice", email: "alice@example.com" },
        { id: 2, name: "Bob", email: "bob@example.com" }
      ]);
    }, 1000);
  });
}

function fetchPostsByUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, userId, title: "Post 1", body: "Content 1" },
        { id: 2, userId, title: "Post 2", body: "Content 2" }
      ]);
    }, 1000);
  });
}

async function loadUserWithPosts(userId) {
  try {
    console.log("Loading user...");
    const users = await fetchUsers();
    const user = users.find(u => u.id === userId);
    
    if (!user) {
      throw new Error("User not found");
    }
    
    console.log("User:", user.name);
    
    console.log("Loading posts...");
    const posts = await fetchPostsByUser(userId);
    
    console.log(`Found ${posts.length} posts:`);
    posts.forEach(post => console.log(`- ${post.title}`));
  } catch (error) {
    console.error("Error:", error);
  }
}

loadUserWithPosts(1);

Example 2: Retry Logic

async function fetchWithRetry<T>(
  fn: () => Promise,
  maxRetries = 3,
  delay = 1000
) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Attempt ${attempt}/${maxRetries}`);
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) {
        throw new Error(`Failed after ${maxRetries} attempts`);
      }
      
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw new Error("Unreachable");
}

// Usage
async function unreliableAPI() {
  if (Math.random() < 0.7) {
    throw new Error("API temporarily unavailable");
  }
  return "Success!";
}

fetchWithRetry(unreliableAPI)
  .then(result => console.log("Result:", result))
  .catch(error => console.log("Final error:", error.message));

Example 3: Batch Processing

async function processBatch<T, R>(
  items: T[],
  processor: (item: T) => Promise,
  batchSize = 3
) {
  const results: R[] = [];
  
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    console.log(`Processing batch ${i / batchSize + 1}`);
    
    const batchResults = await Promise.all(
      batch.map(item => processor(item))
    );
    
    results.push(...batchResults);
  }
  
  return results;
}

// Usage: Process 10 items, 3 at a time
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const processor = async (num) => {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return num * 2;
};

processBatch(numbers, processor, 3)
  .then(results => console.log("Results:", results));

---

Practice Exercises

Exercise 1: Sequential vs Parallel

Create functions that fetch data sequentially and in parallel. Compare timing.

Exercise 2: Error Recovery

Create a function that tries multiple API endpoints until one succeeds.

Exercise 3: Progress Tracker

Create a function that processes items one by one and reports progress.

Exercise 4: Cached Fetcher

Create a function that caches API responses to avoid redundant calls.

Exercise 5: Rate Limiter

Create a function that limits how many concurrent async operations can run.

---

Common Mistakes

Mistake 1: Forgetting await

async function wrong() {
  const data = fetchData();  // ❌ Returns Promise, not data
  console.log(data);  // Promise object, not the data!
}

async function correct() {
  const data = await fetchData();  // ✅ Waits for data
  console.log(data);
}

Mistake 2: Sequential When Could Be Parallel

// ❌ Slow - waits for each
async function slow() {
  const user = await fetchUser();
  const posts = await fetchPosts();  // Doesn't depend on user!
}

// ✅ Fast - runs in parallel
async function fast() {
  const [user, posts] = await Promise.all([
    fetchUser(),
    fetchPosts()
  ]);
}

Mistake 3: Not Handling Errors

// ❌ Unhandled rejection
async function noErrorHandling() {
  const data = await fetchData();  // What if this fails?
}

// ✅ Proper error handling
async function withErrorHandling() {
  try {
    const data = await fetchData();
  } catch (error) {
    console.error("Failed:", error);
  }
}

---

Key Concepts Summary

Concept Purpose Example
Promise Represents future value new Promise((resolve, reject) => {})
async Marks function as asynchronous async function name() {}
await Waits for Promise to resolve const data = await fetchData()
Promise.all() Wait for all promises Promise.all([p1, p2])
Promise.race() First to complete wins Promise.race([p1, p2])
try/catch Handle async errors try { await fn() } catch {}

What You Learned

  • ✅ Difference between sync and async code
  • ✅ How Promises work (pending, fulfilled, rejected)
  • ✅ How to use async/await
  • ✅ Error handling in async code
  • ✅ Running operations in parallel vs sequential
  • ✅ Promise utility methods (all, race, any, allSettled)
  • ✅ Real-world async patterns

What's Next?

In the next lesson, you'll learn about modules and imports - how to organize your code into separate files and reuse code across your project!