Async Patterns

TypeScript Intermediate — Async Patterns
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)) // 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:

  • 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 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 } }
💡 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()) ]); // 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.

→ 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.