Skip to main content

TypeScript Async Programming

TypeScript adds static types to JavaScript's asynchronous features. That makes async code easier to predict and maintain.

This lesson goes from basic async/await to more advanced patterns.

Note: This lesson assumes you already know the basics of JavaScript Promises and asynchronous programming. If you are new to those ideas, learn about JavaScript async first.


Promises in TypeScript

TypeScript makes Promises type-safe through generics.

A Promise<T> is an operation that will finish later with a value of type T, or fail with a reason of type any.

Key points:

  • Promise<T> — generic type where T is the type of the resolved value.
  • Promise<void> — for Promises that do not return a value.
  • Promise<never> — for Promises that never resolve (rare).

Promise states and type flow: a Promise starts as pending and moves either to fulfilled (with a value of type T) or to rejected (with a reason of type any). TypeScript tracks these states through the type system, so you handle both the success and the error case.

The type parameter in Promise<T> tells TypeScript what the Promise will resolve to, which gives better type checking and editor support.

Basic Promise Example

fetchGreeting returns Promise<string>, so inside .then TypeScript knows greeting is a string. The Promise randomly resolves or rejects, so the output is either the upper-cased greeting or the error line.

Prompt
Create a fetchGreeting arrow function typed to return Promise<string>. Inside, build a Promise that waits one second with setTimeout, then randomly (using Math.random() > 0.5) either resolves with "Hello, TypeScript!" or rejects with an Error("Failed to fetch greeting"). Consume it with .then — logging the greeting in upper case, since TypeScript knows it is a string — and .catch with the error typed as Error, printing "Error:" and its message. The output is random: either the upper-cased greeting or the error line.
Output: TypeScript file and captured output

Async/Await with TypeScript

The async/await syntax is a cleaner way to work with Promises. Asynchronous code reads like ordinary step-by-step code, and TypeScript keeps checking the types.

Key benefits of async/await:

  • Readability: sequential code that is easier to follow.
  • Error handling: use try/catch for both sync and async errors.
  • Debugging: stack traces look like synchronous ones.
  • Type safety: full TypeScript inference and checking.

Async function return types. Every async function returns a Promise, and the declared return type must be wrapped in one: async function getString(): string { } is an error, while async function getString(): Promise<string> { } is correct.

Note: The type parameter of the returned Promise is the type you write after Promise in the return type.

Basic Async/Await Example

fetchUsers returns Promise<User[]>, so after await the users variable is a User[] and property access on each user is type-checked.

Prompt
Define a User interface (id, name, email, and role limited to 'admin' | 'user' | 'guest') and an async function fetchUsers(): Promise<User[]> that logs 'Fetching users...', awaits a one-second setTimeout to simulate an API call, then returns Alice (admin) and Bob (user). Write async function processUsers() that awaits fetchUsers() inside try/catch, logs how many users were fetched, filters admins and maps to their emails, logs 'Admin emails:' with that array, and re-throws any error after logging it (checking error instanceof Error). Finally call processUsers() and log 'Processing complete' on success or 'Processing failed:' on failure.
Output: TypeScript file and captured output

Parallel Execution with Promise.all

Run several async operations in parallel and wait for all of them to finish. The three products are fetched at the same time, and their random prices are summed.

Prompt
Define a Product interface (id, name, price) and an async function fetchProduct(id: number): Promise<Product> that logs Fetching product <id>..., waits a random time up to one second, and returns a product named Product <id> with a random whole-number price below 100. Write fetchMultipleProducts() that starts fetchProduct(1), (2) and (3) in parallel with Promise.all, destructures the three results, sums their prices and logs Total price: $<total with two decimals>; log any error with 'Error fetching products:'. Call the function. The total is random.
Output: TypeScript file and captured output

Typing Callbacks for Async Operations

For older callback-style asynchronous code, TypeScript helps you type the callback's parameters so the caller knows exactly what it receives.

Example

The FetchCallback type says the callback gets an Error or null, plus optional string data. Inside the callback TypeScript knows data is a string or undefined.

Prompt
Define a FetchCallback type alias for a callback (error: Error | null, data?: string) => void, and a fetchDataWithCallback(url: string, callback: FetchCallback): void function that simulates async work with a one-second setTimeout, calling the callback with null and "Response data" (or with an Error if something throws). Call it with 'https://api.example.com' and a callback that logs 'Error:' and returns on error, and otherwise logs the data in upper case when it is defined.
Output: TypeScript file and captured output

Promise Combinations

TypeScript gives you typed versions of the methods that combine several Promises. They help you run operations concurrently and handle the results safely.

Promise combination methods:

  • Promise.all() — waits for all promises to resolve.
  • Promise.race() — returns the first settled promise.
  • Promise.allSettled() — waits for all to settle (success or failure).
  • Promise.any() — returns the first fulfilled promise.

Warning: When you pass promises of different types to Promise.all, TypeScript infers a tuple of the individual result types. If the inferred type is not precise enough, use a type assertion or define the expected structure yourself.

Promise.all - Parallel Execution

Run several promises in parallel and wait for all of them. It fails fast if any promise rejects. Here user, posts and stats each get their own precise type from the tuple result.

Prompt
Create three typed arrow functions that return already-resolved Promises: fetchUser(id) giving { id, name: "User <id>" }, fetchPosts(userId) giving two posts (Post 1, Post 2) with ids, and fetchStats(userId) giving { views: 100, likes: 25 }. Write async function loadUserDashboard(userId: number) that awaits all three with Promise.all, logs User: <name>, Posts: <count> and Likes: <likes>, returns the three values in an object, and logs 'Failed to load dashboard:' before re-throwing on error. Call it with user id 1.
Output: TypeScript file and captured output

Promise.race - First to Settle

Useful for timeouts, or for taking the first successful response from several sources. fetchWithTimeout races a promise against a rejecting timer. The functions are only defined, so nothing is printed.

Prompt
Write a timeout(ms: number): Promise<never> helper that rejects with Error("Timeout after <ms>ms") after ms milliseconds, and a generic async function fetchWithTimeout<T>(promise: Promise<T>, timeoutMs = 5000): Promise<T> that races the given promise against the timeout, throwing Request timed out after <ms>ms if the timeout wins. Then define (but do not call) fetchUserData() that uses fetchWithTimeout around fetch('https://api.example.com/user/1') with a 3-second limit, parses the JSON, and on error logs 'Error:' with the message (cast to Error) and re-throws. Nothing is printed because no function is invoked.
Output: TypeScript file and captured output

Promise.allSettled - Handle All Results

Use this when you want to wait for every promise, whether it succeeds or fails. Each result is checked with a type predicate before it is counted as successful or failed. Some requests fail at random, so the counts vary.

Prompt
Create an async fetchData(id: number) that throws Error("Failed to fetch data for ID <id>") when Math.random() > 0.7 and otherwise returns { id, data: "Data for <id>" }. Write async function processBatch(ids: number[]) that maps each id to fetchData(id) wrapped so it resolves to { status: 'fulfilled' as const, value } or { status: 'rejected' as const, reason }, awaits Promise.allSettled on them, separates successful and failed results with type-predicate filters (PromiseFulfilledResult / PromiseRejectedResult), logs Successfully processed: <n> and Failed: <n>, and returns both lists. Call it with ids 1 to 5. The counts are random.
Output: TypeScript file and captured output

Error Handling in Async Code

TypeScript gives you good tools for type-safe error handling in asynchronous code. Here are some patterns and best practices.

Error handling strategies:

  • Try/catch blocks: for handling errors in async/await.
  • Error boundaries: for React components.
  • Result types: a functional approach with success/failure values.
  • Error subclassing: for domain-specific errors.

Error handling patterns worth considering:

  1. Error boundaries in React for component-level handling.
  2. Result objects instead of throwing for expected cases.
  3. Global error handler for uncaught exceptions.
  4. Error logging to capture and report errors.

Custom Error Classes

Create domain-specific error types for better error handling. AppError is the base class, and NetworkError, ValidationError and NotFoundError each carry a code. A local mockFetch stands in for fetch so the example runs without a network.

Prompt
Build a small error hierarchy: an AppError extends Error base class whose constructor takes message, a public readonly code: string and optional public readonly details?: unknown, sets this.name to the class name and captures the stack trace when available; then NetworkError (code 'NETWORK_ERROR'), ValidationError (code 'VALIDATION_ERROR', with a public readonly field), and NotFoundError (code 'NOT_FOUND', message <resource> with ID <id> not found). Write async function fetchUserData(userId: string): Promise<{ id: string; name: string }> that awaits a local mockFetch (a Promise resolving to a fake { ok, status, json() } response so no network is touched), throws NotFoundError on 404, NetworkError on 5xx, a plain Error on other non-ok statuses, and ValidationError('name', ...) when the JSON has no name; unexpected errors are wrapped in AppError with code 'UNEXPECTED_ERROR'. Add displayUserProfile(userId) that logs 'User profile:' with the user, or logs a different console.error prefix (Network issue:, Validation failed:, Not found:, Unexpected error:) per error class, and call it with '123'.
Output: TypeScript file and captured output

Async Iteration with TypeScript

TypeScript supports async iterators and async generators with full typing. A for await loop pulls values out one at a time as they become ready.

Example

generateNumbers is typed as AsyncGenerator<number, void, unknown>, so inside the for await loop num is a number. The functions are only defined here, so nothing is printed.

Prompt
Write an async generator generateNumbers() typed as AsyncGenerator<number, void, unknown> that yields 0 through 4, waiting one second (via setTimeout in an awaited Promise) before each value. Add async function consumeNumbers() that uses for await over the generator and logs each number doubled. Only define the functions; nothing is called, so nothing is printed.
Output: TypeScript file and captured output