Skip to main content

Module 15: Async/Await and Promises

Master asynchronous programming in TypeScript with Promises, async/await, error handling, and advanced patterns for concurrent operations.


1. Promises Basics

function fetchUser(id: number): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Alice", email: "alice@example.com" });
} else {
reject(new Error("Invalid user ID"));
}
}, 1000);
});
}

// Usage
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error(error));

2. Typing Promises

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

// Explicit Promise type
function getUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`)
.then(response => response.json());
}

// Promise with multiple types
function fetchData(): Promise<string | number> {
return Promise.resolve("data");
}

// Promise with union type
type Result<T> = Promise<{ success: true; data: T } | { success: false; error: string }>;

async function getData(): Result<User> {
try {
const user = await getUser(1);
return { success: true, data: user };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}

3. Async/Await Syntax

async function fetchUserData(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const user: User = await response.json();
return user;
}

// Error handling
async function getUserSafely(id: number): Promise<User | null> {
try {
const user = await fetchUserData(id);
return user;
} catch (error) {
console.error("Failed to fetch user:", error);
return null;
}
}

4. Error Handling Patterns

Try-Catch

async function processUser(id: number): Promise<void> {
try {
const user = await fetchUser(id);
console.log(`Processing user: ${user.name}`);
} catch (error) {
if (error instanceof Error) {
console.error(`Error: ${error.message}`);
} else {
console.error("Unknown error occurred");
}
}
}

Custom Error Types

class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = "NotFoundError";
}
}

class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = "ValidationError";
}
}

async function getUser(id: number): Promise<User> {
if (id <= 0) {
throw new ValidationError("id", "ID must be positive");
}

const user = await fetchUser(id);

if (!user) {
throw new NotFoundError(`User with ID ${id} not found`);
}

return user;
}

// Error handling
try {
const user = await getUser(-1);
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation failed on ${error.field}: ${error.message}`);
} else if (error instanceof NotFoundError) {
console.error("Not found:", error.message);
} else {
console.error("Unexpected error:", error);
}
}

5. Promise.all

Execute multiple promises in parallel.

async function fetchMultipleUsers(ids: number[]): Promise<User[]> {
const promises = ids.map(id => fetchUser(id));
return Promise.all(promises);
}

// Usage
const users = await fetchMultipleUsers([1, 2, 3]);
console.log(users);

// With different types
async function fetchAllData() {
const [users, products, orders] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchOrders()
]);

return { users, products, orders };
}

6. Promise.allSettled

Wait for all promises regardless of success/failure.

interface SettledResult<T> {
status: "fulfilled" | "rejected";
value?: T;
reason?: any;
}

async function fetchUsersWithErrors(ids: number[]): Promise<(User | Error)[]> {
const promises = ids.map(id => fetchUser(id));
const results = await Promise.allSettled(promises);

return results.map(result => {
if (result.status === "fulfilled") {
return result.value;
} else {
return new Error(result.reason);
}
});
}

// Usage
const results = await fetchUsersWithErrors([1, -1, 2]);
results.forEach((result, index) => {
if (result instanceof Error) {
console.error(`Failed to fetch user ${index}:`, result.message);
} else {
console.log(`User ${index}:`, result.name);
}
});

7. Promise.race

Return the first resolved promise.

async function fetchWithTimeout<T>(
promise: Promise<T>,
timeout: number
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Timeout")), timeout);
});

return Promise.race([promise, timeoutPromise]);
}

// Usage
try {
const user = await fetchWithTimeout(fetchUser(1), 5000);
console.log(user);
} catch (error) {
console.error("Request timed out");
}

8. Promise.any

Return the first successfully resolved promise.

async function fetchFromMultipleSources<T>(
sources: (() => Promise<T>)[]
): Promise<T> {
const promises = sources.map(source => source());
return Promise.any(promises);
}

// Usage
const user = await fetchFromMultipleSources([
() => fetch("https://api1.com/user").then(r => r.json()),
() => fetch("https://api2.com/user").then(r => r.json()),
() => fetch("https://api3.com/user").then(r => r.json())
]);

9. Sequential Execution

async function processUsersSequentially(ids: number[]): Promise<void> {
for (const id of ids) {
const user = await fetchUser(id);
await processUser(user);
console.log(`Processed user ${id}`);
}
}

// With reduce
async function fetchSequentially<T>(
items: T[],
fetcher: (item: T) => Promise<any>
): Promise<any[]> {
return items.reduce(async (acc, item) => {
const results = await acc;
const result = await fetcher(item);
return [...results, result];
}, Promise.resolve([] as any[]));
}

10. Parallel with Concurrency Limit

async function batchProcess<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
concurrency: number
): Promise<R[]> {
const results: R[] = [];

for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency);
const batchResults = await Promise.all(batch.map(processor));
results.push(...batchResults);
}

return results;
}

// Usage
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const users = await batchProcess(userIds, fetchUser, 3);

11. Retry Logic

async function retry<T>(
fn: () => Promise<T>,
maxAttempts: number = 3,
delay: number = 1000
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error("Max attempts reached");
}

// Usage
const user = await retry(() => fetchUser(1), 3, 2000);

12. Debounce and Throttle

function debounce<T extends (...args: any[]) => Promise<any>>(
fn: T,
delay: number
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
let timeoutId: NodeJS.Timeout | null = null;

return (...args: Parameters<T>): Promise<ReturnType<T>> => {
return new Promise((resolve) => {
if (timeoutId) {
clearTimeout(timeoutId);
}

timeoutId = setTimeout(async () => {
const result = await fn(...args);
resolve(result);
}, delay);
});
};
}

// Usage
const debouncedSearch = debounce(async (query: string) => {
const results = await searchAPI(query);
return results;
}, 300);

13. Async Generators

async function* fetchPagedData(pageSize: number = 10): AsyncGenerator<User[], void> {
let page = 1;
let hasMore = true;

while (hasMore) {
const response = await fetch(`/api/users?page=${page}&size=${pageSize}`);
const data = await response.json();

if (data.length < pageSize) {
hasMore = false;
}

yield data;
page++;
}
}

// Usage
for await (const users of fetchPagedData(10)) {
console.log(`Fetched ${users.length} users`);
users.forEach(user => console.log(user.name));
}

14. Real-World Example: Data Pipeline

interface DataPipeline<T, R> {
transform: (data: T) => Promise<R>;
validate?: (data: R) => boolean;
onError?: (error: Error) => void;
}

class AsyncPipeline<T, R> {
private steps: DataPipeline<any, any>[] = [];

addStep<U>(step: DataPipeline<R, U>): AsyncPipeline<T, U> {
this.steps.push(step);
return this as any;
}

async execute(input: T): Promise<R> {
let result: any = input;

for (const step of this.steps) {
try {
result = await step.transform(result);

if (step.validate && !step.validate(result)) {
throw new Error("Validation failed");
}
} catch (error) {
if (step.onError) {
step.onError(error as Error);
}
throw error;
}
}

return result;
}
}

// Usage
const pipeline = new AsyncPipeline<number, string>()
.addStep({
transform: async (id) => await fetchUser(id)
})
.addStep({
transform: async (user) => await enrichUserData(user),
validate: (user) => user.email !== ""
})
.addStep({
transform: async (user) => JSON.stringify(user)
});

const result = await pipeline.execute(1);

Key Takeaways

Promises represent eventual completion of async operations
async/await provides cleaner syntax than callbacks
✅ Use try-catch for error handling in async functions
Promise.all for parallel execution
Promise.allSettled to handle partial failures
Retry logic for unreliable operations
Async generators for streaming data


Practice Exercises

Exercise 1: Parallel Fetch

async function fetchMultipleResources() {
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json())
]);

return { users, posts, comments };
}

Exercise 2: Retry with Exponential Backoff

Implement a retry function with exponential backoff.

Exercise 3: Async Queue

Create an async queue that processes tasks with concurrency limits.


Next Steps

In Module 16, we'll explore Utility Types and learn about TypeScript's built-in type manipulation utilities.