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.