Back to Curriculum

Async/Await with TypeScript

📚 Lesson 12 of 15 ⏱️ 40 min

Async/Await with TypeScript

40 min

TypeScript's type system provides excellent support for asynchronous programming with async/await, enabling you to write type-safe asynchronous code that catches errors at compile time. Async functions in TypeScript return `Promise<T>`, where T is the type of the value the promise resolves to. This type information flows through your code, ensuring that you handle promises correctly and access resolved values with proper types.

When defining async functions, you can explicitly type the return value as `Promise<T>`, or TypeScript will infer it from the return statement. For example, `async function fetchUser(): Promise<User>` explicitly declares that the function returns a Promise that resolves to a User object. This makes the function's contract clear and enables better IntelliSense and error checking.

Error handling in async functions can be typed using union types or custom error types. A common pattern is to return a `Result<T, E>` type that represents either success with data or failure with an error. This pattern provides type-safe error handling without throwing exceptions. Alternatively, you can use try-catch blocks with properly typed error parameters, though TypeScript's error typing in catch blocks requires careful handling since errors are typed as `unknown` by default in strict mode.

Promise utilities like `Promise.all`, `Promise.allSettled`, and `Promise.race` work seamlessly with TypeScript's type system. `Promise.all<T>` returns `Promise<T[]>` where T is a tuple of the input promise types, preserving type information through the parallel execution. This enables type-safe parallel async operations with full type checking.

TypeScript also supports async generators with `async function*` syntax, which return `AsyncGenerator<T>`. Async generators are useful for streaming data, pagination, and handling sequences of asynchronous operations. The type system ensures that values yielded from async generators are properly typed.

Best practices for async/await in TypeScript include always typing async function return values, using proper error types instead of generic Error, leveraging Promise utilities for parallel operations, and understanding how TypeScript handles promise types. Proper typing of async code prevents common mistakes like forgetting to await promises or accessing promise values incorrectly.

Key Concepts

  • Async functions return Promise<T> where T is the resolved value type.
  • Promise return types can be explicitly declared or inferred by TypeScript.
  • Error handling can be typed using union types or Result types.
  • Promise utilities (all, allSettled, race) preserve type information.
  • Async generators return AsyncGenerator<T> for streaming async data.

Learning Objectives

Master

  • Typing async functions and their return values
  • Handling errors in async functions with proper types
  • Using Promise utilities with type safety
  • Working with async generators and their types

Develop

  • Type-safe asynchronous programming patterns
  • Understanding Promise type flow in TypeScript
  • Designing robust error handling with types

Tips

  • Always type async function return values explicitly for clarity.
  • Use Result<T, E> types for type-safe error handling without exceptions.
  • Leverage Promise.all for type-safe parallel async operations.
  • Type error parameters in catch blocks: catch (error: unknown).

Common Pitfalls

  • Forgetting to await promises, leading to Promise<Promise<T>> types.
  • Not typing async function return values, losing type information.
  • Using any for error types, losing type safety.
  • Not understanding that async functions always return promises.

Summary

  • Async functions return Promise<T> with proper type information.
  • TypeScript infers or allows explicit typing of promise return types.
  • Error handling can be typed using union types or Result patterns.
  • Promise utilities preserve type information for parallel operations.
  • Proper typing of async code prevents common promise-related errors.

Exercise

Create an async function with proper TypeScript typing.

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

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

async function fetchUser(id: number): Promise<ApiResponse<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();
    return {
      data,
      status: response.status,
      message: 'Success'
    };
  } catch (error) {
    throw new Error(`Failed to fetch user: ${error}`);
  }
}

// Usage
fetchUser(1).then(result => {
  console.log(result.data.name);
}).catch(error => {
  console.error(error);
});

Exercise Tips

  • Type error handling: catch (error: unknown) { if (error instanceof Error) { ... } }
  • Use Promise.all for parallel async operations: Promise.all([fetchUser(1), fetchUser(2)]);
  • Create a Result type for better error handling: type Result<T, E> = { success: true, data: T } | { success: false, error: E };
  • Use async generators: async function* fetchUsers(): AsyncGenerator<User> { ... }

Code Editor

Output