TypeScript Intermediate
Course 2 · Chapter 4 · Async Patterns
⚡ Async Patterns
Modern JavaScript is asynchronous. APIs don't return instantly; databases take time; user interactions are unpredictable. This chapter teaches you to write type-safe async code that handles delays, errors, and concurrent operations without callback hell.
Promises: The Foundation
A Promise represents a value that will (eventually) resolve or reject:
const promise: Promise<string> = new Promise<string>((resolve, reject) => {
setTimeout(() => {
resolve("Success!");
}, 1000);
});
promise
.then((result) => console.log(result))
.catch((error) => console.error(error));
Pending
Initial state. The async operation is still running.
Fulfilled
Operation succeeded. The Promise holds a value.
Rejected
Operation failed. The Promise holds a reason (error).
Settled
Either fulfilled or rejected. No longer pending.
Type annotation: Promise<T> means "a Promise that resolves to T".
🎯 Async/Await: Synchronous-Looking Code
Async/await is syntactic sugar for Promises. It makes async code look like sync code:
Promises vs Async/Await
Promise .then() chain
function fetchUser(id: number) {
return fetch(`/api/users/${id}`)
.then(r => r.json())
.then(user => {
console.log(user);
return user;
})
.catch(err => {
console.error(err);
throw err;
});
}
Async/await
async function fetchUser(id: number) {
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
console.log(user);
return user;
} catch (err) {
console.error(err);
throw err;
}
}
Key syntax:
async before the function declares it returns a Promise
await pauses execution until a Promise settles
try/catch handles rejections like sync errors
Typing Async Functions
async function getUser(id: number): Promise<{ id: number; name: string }> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
const user = await getUser(1);
const promise = getUser(1);
🛡️ Error Handling in Async Code
Always wrap async code in try/catch:
async function processData() {
try {
const data = await fetch("/api/data").then(r => r.json());
if (!data) throw new Error("No data");
return data;
} catch (error) {
if (error instanceof TypeError) {
console.error("Network error:", error.message);
} else if (error instanceof Error) {
console.error("Error:", error.message);
}
throw error;
}
}
💡 Always Catch or Re-throw
Unhandled Promise rejections crash silently. Either catch the error or re-throw it to propagate up the stack. If you intentionally ignore an error, at least add a `.catch(() => {})` to make it explicit.
🚀 Concurrent Operations: Running Multiple Promises
Promise.all() waits for all Promises to resolve. If any reject, it rejects:
async function fetchMultiple() {
try {
const [user, posts, comments] = await Promise.all([
fetch("/api/user").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json())
]);
return { user, posts, comments };
} catch (error) {
console.error("One or more requests failed", error);
throw error;
}
}
Promise.all()
All succeed → returns array. Any fails → rejects immediately.
Promise.allSettled()
Waits for all, regardless of success/failure. Returns array of outcomes.
Promise.race()
First to settle (success or failure) wins. Returns first result.
Promise.any()
First to succeed wins. If all fail, rejects with aggregate error.
Example: Resilient Concurrent Requests
async function fetchAll() {
const results = await Promise.allSettled([
fetch("/api/critical"),
fetch("/api/optional")
]);
const [critical, optional] = results;
if (critical.status === "fulfilled") {
console.log("Critical data:", critical.value);
} else {
throw Error("Critical request failed");
}
if (optional.status === "fulfilled") {
console.log("Optional data:", optional.value);
} else {
console.warn("Optional request failed, continuing anyway");
}
}
Async Iteration: for await...of
For loops that wait on each iteration:
async function * fetchPages(pageCount: number) {
for (let i = 1; i <= pageCount; i++) {
const data = await fetch(`/api/page/${i}`).then(r => r.json());
yield data;
}
}
async function processPages() {
for await (const page of fetchPages(5)) {
console.log("Processing page:", page);
}
}
🏗️ Real-World Pattern: Retry with Exponential Backoff
Resilient API Call with Retries
async function fetchWithRetry<T>(
url: string,
maxRetries: number = 3
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = 100 * Math.pow(2, attempt);
console.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
const data = await fetchWithRetry<{ id: number; name: string }>("/api/data");
💻 Coding Challenges
Challenge 1: Async Function with Error Handling
Write an async function that fetches data from a URL, parses JSON, and handles network errors separately from parse errors. Return the data or throw an appropriate error.
Goal: Practice async/await and error handling.
→ Solution
Challenge 2: Concurrent Requests
Write an async function that fetches data from 3 different URLs concurrently using Promise.all(). Handle the case where one fails and all fail.
Goal: Understand concurrent operations and failure modes.
→ Solution
Challenge 3: Retry Logic
Build a function that retries a failing async operation (simulated by a random success/failure) up to N times before giving up.
Goal: Implement retry patterns with loops and timing.
→ Solution
⚠️ Gotcha: Forgetting to Await
Forgetting await returns a Promise instead of the value. The code still runs (no error), but you're working with a Promise when you expected the value. Always use await inside async functions when you need the result, or use .then() if not in an async context.
🎯 What's Next
With async patterns mastered, we'll explore Module Systems & Namespaces — how to organize code across files and manage dependencies in TypeScript.