⚡IntermediateAsync & Error Handling

Async TypeScript

Type async functions, Promise chains, Promise.all, async generators, and handle async error types correctly in TypeScript.

Overview

TypeScript has full support for async/await and Promises. The key insight is that an `async` function always returns `Promise<T>` where T is the type of the value you `return`. TypeScript infers this automatically. **Promise combinators** like `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` each have precise TypeScript types that preserve the types of individual promises - `Promise.all([p1, p2])` where p1 is `Promise<string>` and p2 is `Promise<number>` returns `Promise<[string, number]>`. **Async generators** (`async function*`) return `AsyncGenerator<T>` and let you `yield` values over time, perfect for streaming APIs. TypeScript types each `yield` value and the final `return` value separately. One important caveat: TypeScript cannot type the `catch` clause error variable as anything other than `unknown` in strict mode - because any type of value can be thrown. You must narrow it before use.

Key Points

  • •Async function return type: `async function f(): Promise<string>` - or let TypeScript infer it
  • •`Promise<T>` is the type of a pending value that resolves to T
  • •`Awaited<T>` is the utility type that unwraps a Promise: `Awaited<Promise<string>>` = `string`
  • •`Promise.all([p1, p2])` infers a tuple: `Promise<[T1, T2]>` - preserves individual types
  • •`Promise.allSettled` returns `PromiseSettledResult<T>[]` - each is either fulfilled or rejected
  • •Catch clause: `catch (e)` - `e` is typed as `unknown`; must narrow with `instanceof Error`
  • •Async generator: `async function*` returns `AsyncGenerator<YieldType, ReturnType, NextType>`

Examples

Typed async functions and inference

interface User {
  id: number;
  name: string;
  email: string;
}

// TypeScript infers return type as Promise<User>
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json(); // Promise<any>  -  we trust the API contract
}

// Chaining with typed steps
async function getUserName(id: number): Promise<string> {
  const user = await fetchUser(id); // User
  return user.name;                 // string  -  TypeScript tracks this
}

// Async IIFE (immediately invoked)
(async () => {
  try {
    const name = await getUserName(1);
    console.log(name); // string
  } catch (error) {
    // error is 'unknown' in strict mode  -  must narrow
    if (error instanceof Error) {
      console.error(error.message);
    }
  }
})();

Promise combinators with precise types

async function fetchString(): Promise<string>  { return 'hello'; }
async function fetchNumber(): Promise<number>  { return 42; }
async function fetchBool():   Promise<boolean> { return true; }

// Promise.all preserves tuple types
const [s, n, b] = await Promise.all([
  fetchString(), // Promise<string>
  fetchNumber(), // Promise<number>
  fetchBool(),   // Promise<boolean>
]);
// s: string, n: number, b: boolean

// Promise.allSettled  -  each result is discriminated union
const results = await Promise.allSettled([fetchString(), fetchNumber()]);
for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log('Value:', result.value); // string | number
  } else {
    console.log('Error:', result.reason); // unknown
  }
}

// Promise.race  -  returns the type of the first to settle
const first = await Promise.race([fetchString(), fetchNumber()]);
// first: string | number

// Promise.any  -  same as race but ignores rejections
const winner = await Promise.any([fetchString(), fetchNumber()]);
// winner: string | number

Async generators for streaming data

// AsyncGenerator<YieldType, ReturnType, NextType>
async function* paginate<T>(
  fetchPage: (page: number) => Promise<T[]>,
  maxPages = 10
): AsyncGenerator<T, void, unknown> {
  for (let page = 1; page <= maxPages; page++) {
    const items = await fetchPage(page);
    if (items.length === 0) return; // done
    for (const item of items) {
      yield item; // TypeScript knows this is T
    }
  }
}

// Simulated API
async function fetchUsersPage(page: number): Promise<User[]> {
  if (page > 3) return [];
  return [
    { id: page * 10 + 1, name: `User${page}A`, email: '' },
    { id: page * 10 + 2, name: `User${page}B`, email: '' },
  ];
}

// Consume with for-await-of
for await (const user of paginate(fetchUsersPage)) {
  console.log(user.name); // User is fully typed
}

interface User { id: number; name: string; email: string }

Common Mistakes

  • Using `Promise<any>` as a return type - always type the resolved value; `Promise<User>` is far safer than `Promise<any>`
  • Not handling the `unknown` type in catch clauses: `catch (e) { e.message }` errors in strict mode; use `if (e instanceof Error)` first
  • Floating promises - calling an async function without `await` or `.catch()` silently drops errors; TypeScript with `"noFloatingPromises"` in eslint or `"noUncheckedSideEffectImports"` can catch this

Note

Use `Awaited<ReturnType<typeof myAsyncFunction>>` to extract the resolved type of any async function without repeating the type. It works with nested promises too: `Awaited<Promise<Promise<string>>>` = `string`.

Exercises

Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.

Loading editor…

Completed: 0 / 3 exercises

Knowledge Check

Question 1 of 4

What is the return type of `async function greet(): Promise<string> { return "hello" }`?