Async Patterns
โก Async Patterns
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)) // Logs "Success!" after 1s .catch((error) => console.error(error)); // Catches rejection
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:
asyncbefore the function declares it returns a Promiseawaitpauses execution until a Promise settlestry/catchhandles rejections like sync errors
Typing Async Functions
// Async function always returns a Promise async function getUser(id: number): Promise<{ id: number; name: string }> { const response = await fetch(`/api/users/${id}`); return response.json(); } // You DON'T write "Promise<Promise<T>>" even though await unwraps // TypeScript automatically unwraps for you const user = await getUser(1); // type: { id: number; name: string } // If you don't await, you get the Promise: const promise = getUser(1); // type: Promise<{ id: number; name: string }>
๐ก๏ธ 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; // Re-throw to propagate } }
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()) ]); // All three fetches ran concurrently โ 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"), // Must succeed fetch("/api/optional") // Nice to have ]); 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; // Exponential backoff: 100ms, 200ms, 400ms, ... const delay = 100 * Math.pow(2, attempt); console.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // Usage 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.
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.
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.
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.