TypeScript Async Programming

TypeScript types every asynchronous value as a Promise<T>, letting the compiler track what a pending operation will eventually resolve to.

Typing Promises

A `Promise<T>` represents a value of type `T` that isn't available yet. Whatever you pass to `resolve` inside the promise executor must match `T`, and anything you get back from `.then()` is typed as `T` as well, so the compiler catches mismatches long before the promise ever settles.

A Basic Typed Promise

function fetchUser(id: number): Promise<{ id: number; name: string }> {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: "Ada Lovelace" }), 100);
  });
}

fetchUser(1).then((user) => console.log(user.name));

async/await and Inferred Return Types

A function marked `async` always returns a `Promise`, even if you write a plain `return` inside it - TypeScript wraps the return type for you. Inside the function, `await` unwraps a `Promise<T>` down to `T`, and a `try`/`catch` around it lets you handle rejected promises the same way you'd handle a thrown synchronous error.

async/await with Error Handling

function fetchUser(id: number): Promise<{ id: number; name: string }> {
  return Promise.resolve({ id, name: "Grace Hopper" });
}

async function loadUserName(id: number): Promise<string> {
  try {
    const user = await fetchUser(id);
    return user.name;
  } catch (error) {
    console.error("Failed to load user", error);
    return "unknown";
  }
}

loadUserName(2).then((name) => console.log(name));

Running Promises in Parallel

Awaiting promises one after another runs them sequentially, which wastes time when the operations don't depend on each other. `Promise.all` starts every promise at once and, in TypeScript, infers a typed tuple that matches the exact type of each input promise once they all resolve.

Promise.all with a Typed Tuple

function fetchUser(id: number): Promise<{ id: number; name: string }> {
  return Promise.resolve({ id, name: "Linus" });
}

function fetchPosts(userId: number): Promise<string[]> {
  return Promise.resolve(["Hello world", "TypeScript tips"]);
}

async function loadDashboard(userId: number): Promise<void> {
  const [user, posts] = await Promise.all([fetchUser(userId), fetchPosts(userId)]);
  console.log(user.name, posts.length);
}

loadDashboard(1);
  • Promise.all - waits for every promise, rejects as soon as one rejects
  • Promise.allSettled - waits for every promise and never rejects
  • Promise.race - settles as soon as the first promise settles
  • Promise.any - resolves as soon as the first promise fulfills
CombinatorResolves whenRejects when
Promise.allEvery promise fulfillsAny promise rejects
Promise.allSettledEvery promise settlesNever rejects
Promise.raceFirst promise to settle winsFirst settle is a rejection
Promise.anyFirst promise fulfillsAll promises reject
Note: If you intentionally don't need to await a promise, prefix the call with `void` (e.g. `void logAnalyticsEvent()`) - it documents the intent and keeps linters like `no-floating-promises` quiet without swallowing errors silently.
Note: Awaiting inside a `for` loop runs each iteration sequentially even when the calls are independent - if order doesn't matter, collect the promises first and pass them to `Promise.all` instead.

Exercise: TypeScript Async Programming

What does an async function always return, regardless of what its body returns?