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:
- Pending: Operation hasn't finished yet
- Fulfilled: Operation succeeded (resolved)
- 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
awaitonly works insideasyncfunctionsasyncfunctions always return a Promise- 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!