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.