Prerequisites
- Basic TypeScript syntax and types π
- Understanding of Promises and async programming β‘
- Promise<T> types and error handling π‘οΈ
What you'll learn
- Master promise chaining for sequential async operations π
- Handle complex data transformations with type safety β¨
- Implement robust error handling in promise chains π¨
- Build elegant async workflows and data pipelines ποΈ
π― Introduction
Welcome to the art of promise chaining in TypeScript! π In this guide, weβll explore how to create elegant sequences of asynchronous operations that flow like a well-orchestrated symphony.
Youβll discover how to chain promises together to create complex workflows that are both type-safe and maintainable. Whether youβre processing user data π€, calling multiple APIs π, or building data transformation pipelines π, mastering promise chaining is essential for creating sophisticated async applications.
By the end of this tutorial, youβll be crafting async sequences that are not just functional, but beautifully composed and impossible to break! π Letβs dive in! πββοΈ
π Understanding Promise Chaining
π€ What is Promise Chaining?
Promise chaining is like creating a production line for async operations π. Think of it as a series of conveyor belts where each step processes the output from the previous step, transforming data as it flows through your application.
In TypeScript terms, promise chaining provides:
- β¨ Sequential execution - operations happen one after another
- π Data transformation - each step can modify the result
- π‘οΈ Type safety - TypeScript tracks types through the chain
- π¦ Clean syntax - readable, maintainable async code
π‘ Why Use Promise Chaining?
Hereβs why promise chaining is powerful:
- Sequential Dependencies π: When operation B needs result from A
- Data Transformation π: Transform data through multiple steps
- Clean Error Handling π¨: Single catch for the entire chain
- Readable Code π: Linear flow thatβs easy to follow
- Type Safety π: Maintain types through complex transformations
Real-world example: When a user logs in π, you might: authenticate β fetch profile β load preferences β redirect to dashboard. Each step depends on the previous one!
π§ Basic Promise Chaining Patterns
π Simple Chaining with .then()
Letβs start with fundamental chaining patterns:
// π― Basic promise chaining syntax
// Each .then() receives the result from the previous step
// π Simple data transformation chain
const processUserInput = (input: string): Promise<string> => {
return Promise.resolve(input)
.then((data: string) => {
// π§Ή Step 1: Clean the input
console.log('π§Ή Cleaning input:', data);
return data.trim().toLowerCase();
})
.then((cleaned: string) => {
// β
Step 2: Validate the input
console.log('β
Validating input:', cleaned);
if (cleaned.length < 3) {
throw new Error('Input too short! π');
}
return cleaned;
})
.then((validated: string) => {
// π¨ Step 3: Format the output
console.log('π¨ Formatting output:', validated);
return `processed_${validated}`;
});
};
// π Usage
processUserInput(' Hello World ')
.then(result => {
console.log('β¨ Final result:', result); // processed_hello world
})
.catch(error => {
console.error('π₯ Error:', error.message);
});
π Chaining with Type Transformations
// π― Complex type transformations through chaining
interface RawUser {
id: number;
name: string;
email: string;
created_at: string;
}
interface ProcessedUser {
id: string;
displayName: string;
email: string;
memberSince: Date;
isActive: boolean;
}
interface UserProfile {
user: ProcessedUser;
permissions: string[];
preferences: Record<string, any>;
}
// π Multi-step user processing pipeline
const processUserData = (rawUser: RawUser): Promise<UserProfile> => {
return Promise.resolve(rawUser)
.then((raw: RawUser): ProcessedUser => {
// π Step 1: Transform user data
console.log('π Transforming user data for:', raw.name);
return {
id: `user_${raw.id}`,
displayName: raw.name.split(' ')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(' '),
email: raw.email.toLowerCase(),
memberSince: new Date(raw.created_at),
isActive: true
};
})
.then((user: ProcessedUser): Promise<UserProfile> => {
// π Step 2: Load user permissions (async operation)
console.log('π Loading permissions for:', user.displayName);
return loadUserPermissions(user.id).then(permissions => ({
user,
permissions,
preferences: {} // Initialize empty preferences
}));
})
.then((profile: UserProfile): Promise<UserProfile> => {
// βοΈ Step 3: Load user preferences (another async operation)
console.log('βοΈ Loading preferences for:', profile.user.displayName);
return loadUserPreferences(profile.user.id).then(preferences => ({
...profile,
preferences
}));
});
};
// π οΈ Helper functions for async operations
const loadUserPermissions = async (userId: string): Promise<string[]> => {
// π Simulate API call
await new Promise(resolve => setTimeout(resolve, 100));
return ['read', 'write', 'delete'];
};
const loadUserPreferences = async (userId: string): Promise<Record<string, any>> => {
// π Simulate API call
await new Promise(resolve => setTimeout(resolve, 150));
return {
theme: 'dark',
language: 'en',
notifications: true
};
};
π Real-World API Chaining Examples
π‘ Multi-API Data Aggregation
// π― Real-world example: E-commerce order processing
interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface User {
id: string;
name: string;
email: string;
shippingAddress: Address;
}
interface Address {
street: string;
city: string;
country: string;
zipCode: string;
}
interface Order {
id: string;
user: User;
products: Product[];
total: number;
shippingCost: number;
tax: number;
finalTotal: number;
}
interface ProcessedOrder {
order: Order;
paymentIntentId: string;
trackingNumber: string;
estimatedDelivery: Date;
}
// π Complex order processing pipeline
const processOrder = (userId: string, productIds: string[]): Promise<ProcessedOrder> => {
return fetchUser(userId)
.then((user: User) => {
// π€ Step 1: Get user data
console.log('π€ User loaded:', user.name);
// π¦ Step 2: Fetch all products in parallel, then continue chain
return Promise.all(productIds.map(id => fetchProduct(id)))
.then((products: Product[]) => ({ user, products }));
})
.then(({ user, products }) => {
// π° Step 3: Calculate order totals
console.log('π° Calculating totals for', products.length, 'products');
const subtotal = products.reduce((sum, product) => sum + product.price, 0);
const tax = subtotal * 0.08; // 8% tax
const shippingCost = calculateShipping(user.shippingAddress, products);
const finalTotal = subtotal + tax + shippingCost;
const order: Order = {
id: `order_${Date.now()}`,
user,
products,
total: subtotal,
shippingCost,
tax,
finalTotal
};
return order;
})
.then((order: Order) => {
// π³ Step 4: Process payment
console.log('π³ Processing payment for $', order.finalTotal);
return processPayment(order).then(paymentIntentId => ({
order,
paymentIntentId
}));
})
.then(({ order, paymentIntentId }) => {
// π Step 5: Create shipping label
console.log('π Creating shipping label');
return createShippingLabel(order).then(trackingNumber => ({
order,
paymentIntentId,
trackingNumber
}));
})
.then(({ order, paymentIntentId, trackingNumber }) => {
// π
Step 6: Calculate delivery estimate
console.log('π
Calculating delivery estimate');
const estimatedDelivery = calculateDeliveryDate(
order.user.shippingAddress,
order.products
);
const processedOrder: ProcessedOrder = {
order,
paymentIntentId,
trackingNumber,
estimatedDelivery
};
return processedOrder;
});
};
// π οΈ Mock API functions
const fetchUser = async (userId: string): Promise<User> => {
await new Promise(resolve => setTimeout(resolve, 200));
return {
id: userId,
name: 'John Doe',
email: '[email protected]',
shippingAddress: {
street: '123 Main St',
city: 'New York',
country: 'USA',
zipCode: '10001'
}
};
};
const fetchProduct = async (productId: string): Promise<Product> => {
await new Promise(resolve => setTimeout(resolve, 100));
return {
id: productId,
name: `Product ${productId}`,
price: Math.floor(Math.random() * 100) + 10,
category: 'Electronics'
};
};
const processPayment = async (order: Order): Promise<string> => {
await new Promise(resolve => setTimeout(resolve, 300));
return `pi_${Math.random().toString(36).substr(2, 9)}`;
};
const createShippingLabel = async (order: Order): Promise<string> => {
await new Promise(resolve => setTimeout(resolve, 200));
return `TRK${Math.random().toString(36).substr(2, 8).toUpperCase()}`;
};
const calculateShipping = (address: Address, products: Product[]): number => {
const baseRate = 5.99;
const weightRate = products.length * 1.50;
return Math.round((baseRate + weightRate) * 100) / 100;
};
const calculateDeliveryDate = (address: Address, products: Product[]): Date => {
const daysToAdd = address.country === 'USA' ? 3 : 7;
const deliveryDate = new Date();
deliveryDate.setDate(deliveryDate.getDate() + daysToAdd);
return deliveryDate;
};
π¨ Error Handling in Promise Chains
π‘οΈ Comprehensive Error Management
// π― Robust error handling strategies
interface ApiError {
code: string;
message: string;
statusCode: number;
timestamp: Date;
}
interface RetryOptions {
maxRetries: number;
delayMs: number;
backoffMultiplier: number;
}
// π Promise chain with retry logic and error recovery
const robustApiCall = <T>(
operation: () => Promise<T>,
retryOptions: RetryOptions = {
maxRetries: 3,
delayMs: 1000,
backoffMultiplier: 2
}
): Promise<T> => {
let attempt = 0;
const executeWithRetry = (): Promise<T> => {
return operation()
.catch((error: Error) => {
attempt++;
// π¨ Log the error attempt
console.error(`β Attempt ${attempt} failed:`, error.message);
if (attempt >= retryOptions.maxRetries) {
// π Max retries reached, give up
throw new Error(`Operation failed after ${retryOptions.maxRetries} attempts: ${error.message}`);
}
// β±οΈ Calculate delay with exponential backoff
const delay = retryOptions.delayMs * Math.pow(retryOptions.backoffMultiplier, attempt - 1);
console.log(`β³ Retrying in ${delay}ms... (attempt ${attempt + 1}/${retryOptions.maxRetries})`);
// π Wait and retry
return new Promise<T>((resolve) => {
setTimeout(() => {
resolve(executeWithRetry());
}, delay);
});
});
};
return executeWithRetry();
};
// π₯ Error recovery and fallback strategies
const resilientDataPipeline = (userId: string): Promise<UserProfile> => {
return Promise.resolve(userId)
.then((id: string) => {
// π― Step 1: Fetch user with retry logic
console.log('π€ Fetching user data...');
return robustApiCall(() => fetchUserWithError(id));
})
.then((user: ProcessedUser) => {
// π Step 2: Try to load permissions, with fallback
console.log('π Loading user permissions...');
return loadUserPermissions(user.id)
.catch((error: Error) => {
// π¨ If permissions fail, provide default permissions
console.warn('β οΈ Failed to load permissions, using defaults:', error.message);
return ['read']; // Default fallback permissions
})
.then((permissions: string[]) => ({ user, permissions }));
})
.then(({ user, permissions }) => {
// βοΈ Step 3: Try to load preferences, with fallback
console.log('βοΈ Loading user preferences...');
return loadUserPreferences(user.id)
.catch((error: Error) => {
// π¨ If preferences fail, provide defaults
console.warn('β οΈ Failed to load preferences, using defaults:', error.message);
return { theme: 'light', language: 'en' }; // Default preferences
})
.then((preferences: Record<string, any>) => ({
user,
permissions,
preferences
}));
})
.catch((error: Error) => {
// π₯ Final error handler for the entire chain
console.error('π₯ Pipeline failed completely:', error.message);
// π Create minimal profile for graceful degradation
const fallbackProfile: UserProfile = {
user: {
id: userId,
displayName: 'Unknown User',
email: '[email protected]',
memberSince: new Date(),
isActive: false
},
permissions: ['read'],
preferences: { theme: 'light' }
};
return fallbackProfile;
});
};
// π Mock function that randomly fails for testing
const fetchUserWithError = async (userId: string): Promise<ProcessedUser> => {
await new Promise(resolve => setTimeout(resolve, 200));
// π² 30% chance of failure for testing
if (Math.random() < 0.3) {
throw new Error(`User service temporarily unavailable π«`);
}
return {
id: userId,
displayName: 'John Doe',
email: '[email protected]',
memberSince: new Date(),
isActive: true
};
};
π Advanced Chaining Patterns
π¨ Conditional Chaining and Branching
// π― Dynamic promise chains based on conditions
interface TaskResult {
success: boolean;
data?: any;
error?: string;
timestamp: Date;
}
interface ProcessingContext {
userId: string;
taskType: 'premium' | 'standard' | 'basic';
priority: number;
requiresApproval: boolean;
}
// πΏ Conditional processing pipeline
const conditionalProcessing = (context: ProcessingContext): Promise<TaskResult> => {
return Promise.resolve(context)
.then((ctx: ProcessingContext) => {
// π― Step 1: Basic validation
console.log('π Validating context for user:', ctx.userId);
if (!ctx.userId) {
throw new Error('User ID is required π');
}
return ctx;
})
.then((ctx: ProcessingContext) => {
// π·οΈ Step 2: Conditional processing based on task type
console.log('π·οΈ Processing', ctx.taskType, 'task');
switch (ctx.taskType) {
case 'premium':
return processPremiumTask(ctx);
case 'standard':
return processStandardTask(ctx);
case 'basic':
return processBasicTask(ctx);
default:
throw new Error(`Unknown task type: ${ctx.taskType} π€`);
}
})
.then((result: any) => ({
success: true,
data: result,
timestamp: new Date()
}))
.catch((error: Error) => ({
success: false,
error: error.message,
timestamp: new Date()
}));
};
// π Premium task processing with extra steps
const processPremiumTask = (context: ProcessingContext): Promise<any> => {
return Promise.resolve(context)
.then((ctx) => {
// π Premium feature: Priority processing
console.log('π Applying premium priority processing');
return { ...ctx, priorityApplied: true };
})
.then((ctx) => {
// π¨ Premium feature: Advanced analytics
console.log('π Running premium analytics');
return runAdvancedAnalytics(ctx);
})
.then((analyticsResult) => {
// β¨ Premium feature: Custom notifications
console.log('π± Sending premium notifications');
return sendPremiumNotification(analyticsResult);
});
};
// π Standard task processing
const processStandardTask = (context: ProcessingContext): Promise<any> => {
return Promise.resolve(context)
.then((ctx) => {
console.log('π Running standard processing');
return { processedAt: new Date(), type: 'standard' };
})
.then((result) => {
// π Standard notifications
console.log('π Sending standard notification');
return { ...result, notificationSent: true };
});
};
// π Basic task processing
const processBasicTask = (context: ProcessingContext): Promise<any> => {
return Promise.resolve({
processedAt: new Date(),
type: 'basic',
message: 'Basic task completed β
'
});
};
// π οΈ Helper functions
const runAdvancedAnalytics = async (context: any): Promise<any> => {
await new Promise(resolve => setTimeout(resolve, 500));
return { ...context, analyticsScore: Math.random() * 100 };
};
const sendPremiumNotification = async (data: any): Promise<any> => {
await new Promise(resolve => setTimeout(resolve, 200));
return { ...data, premiumNotificationSent: true };
};
π Parallel Processing within Chains
// π― Combining parallel and sequential operations
interface DataSource {
id: string;
type: 'api' | 'database' | 'cache';
priority: number;
}
interface AggregatedData {
userId: string;
profileData: any;
activityData: any;
settingsData: any;
combinedAt: Date;
}
// π Mixed parallel/sequential data aggregation
const aggregateUserData = (userId: string): Promise<AggregatedData> => {
return Promise.resolve(userId)
.then((id: string) => {
// π― Step 1: Sequential - validate user exists
console.log('β
Validating user exists:', id);
return validateUserExists(id);
})
.then((validUserId: string) => {
// π Step 2: Parallel - fetch multiple data sources simultaneously
console.log('π Fetching data from multiple sources...');
const dataPromises = [
fetchUserProfile(validUserId),
fetchUserActivity(validUserId),
fetchUserSettings(validUserId)
];
return Promise.all(dataPromises).then(([profileData, activityData, settingsData]) => ({
userId: validUserId,
profileData,
activityData,
settingsData
}));
})
.then(({ userId, profileData, activityData, settingsData }) => {
// π Step 3: Sequential - process and combine data
console.log('π Processing and combining data...');
return processAndCombineData(profileData, activityData, settingsData)
.then((combinedData) => ({
userId,
...combinedData,
combinedAt: new Date()
}));
})
.then((aggregatedData: AggregatedData) => {
// πΎ Step 4: Sequential - cache the result
console.log('πΎ Caching aggregated data...');
return cacheAggregatedData(aggregatedData).then(() => aggregatedData);
});
};
// π οΈ Helper functions for the pipeline
const validateUserExists = async (userId: string): Promise<string> => {
await new Promise(resolve => setTimeout(resolve, 100));
if (!userId || userId.length < 3) {
throw new Error('Invalid user ID π«');
}
return userId;
};
const fetchUserProfile = async (userId: string): Promise<any> => {
console.log('π€ Fetching profile...');
await new Promise(resolve => setTimeout(resolve, 200));
return { name: 'John Doe', email: '[email protected]' };
};
const fetchUserActivity = async (userId: string): Promise<any> => {
console.log('π Fetching activity...');
await new Promise(resolve => setTimeout(resolve, 300));
return { lastLogin: new Date(), sessionsCount: 42 };
};
const fetchUserSettings = async (userId: string): Promise<any> => {
console.log('βοΈ Fetching settings...');
await new Promise(resolve => setTimeout(resolve, 150));
return { theme: 'dark', language: 'en' };
};
const processAndCombineData = async (profile: any, activity: any, settings: any): Promise<any> => {
await new Promise(resolve => setTimeout(resolve, 100));
return {
profileData: profile,
activityData: activity,
settingsData: settings,
processed: true
};
};
const cacheAggregatedData = async (data: AggregatedData): Promise<void> => {
await new Promise(resolve => setTimeout(resolve, 50));
console.log('β
Data cached successfully');
};
π― Practical Exercise: Blog Post Publishing Pipeline
// π― Complete blog publishing workflow
interface BlogPost {
id: string;
title: string;
content: string;
authorId: string;
tags: string[];
status: 'draft' | 'review' | 'published';
createdAt: Date;
publishedAt?: Date;
}
interface Author {
id: string;
name: string;
email: string;
isVerified: boolean;
publishingRights: boolean;
}
interface PublishResult {
post: BlogPost;
seoScore: number;
socialMediaPosted: boolean;
notificationsSent: string[];
publicUrl: string;
}
// π Complete blog publishing pipeline
const publishBlogPost = (postId: string): Promise<PublishResult> => {
return Promise.resolve(postId)
.then((id: string) => {
// π Step 1: Fetch the blog post
console.log('π Fetching blog post:', id);
return fetchBlogPost(id);
})
.then((post: BlogPost) => {
// π€ Step 2: Verify author permissions
console.log('π€ Verifying author permissions for:', post.authorId);
return fetchAuthor(post.authorId).then((author: Author) => {
if (!author.isVerified || !author.publishingRights) {
throw new Error(`Author ${author.name} lacks publishing permissions π«`);
}
return { post, author };
});
})
.then(({ post, author }) => {
// β
Step 3: Content validation and SEO analysis
console.log('β
Validating content and analyzing SEO...');
return Promise.all([
validateContent(post),
analyzeSEO(post)
]).then(([isValid, seoScore]) => {
if (!isValid) {
throw new Error('Content validation failed π');
}
return { post, author, seoScore };
});
})
.then(({ post, author, seoScore }) => {
// π Step 4: Publish the post
console.log('π Publishing blog post...');
const publishedPost: BlogPost = {
...post,
status: 'published',
publishedAt: new Date()
};
return savePublishedPost(publishedPost).then(() => ({
post: publishedPost,
author,
seoScore
}));
})
.then(({ post, author, seoScore }) => {
// π± Step 5: Social media and notifications (parallel)
console.log('π± Posting to social media and sending notifications...');
return Promise.all([
postToSocialMedia(post),
sendNotifications(post, author)
]).then(([socialMediaPosted, notificationsSent]) => ({
post,
seoScore,
socialMediaPosted,
notificationsSent
}));
})
.then(({ post, seoScore, socialMediaPosted, notificationsSent }) => {
// π Step 6: Generate public URL
console.log('π Generating public URL...');
const publicUrl = generatePublicUrl(post);
const result: PublishResult = {
post,
seoScore,
socialMediaPosted,
notificationsSent,
publicUrl
};
return result;
});
};
// π οΈ Mock implementation functions
const fetchBlogPost = async (postId: string): Promise<BlogPost> => {
await new Promise(resolve => setTimeout(resolve, 100));
return {
id: postId,
title: 'How to Master Promise Chaining',
content: 'Promise chaining is a powerful pattern...',
authorId: 'author_123',
tags: ['typescript', 'async', 'promises'],
status: 'review',
createdAt: new Date(Date.now() - 86400000) // 1 day ago
};
};
const fetchAuthor = async (authorId: string): Promise<Author> => {
await new Promise(resolve => setTimeout(resolve, 50));
return {
id: authorId,
name: 'Jane Developer',
email: '[email protected]',
isVerified: true,
publishingRights: true
};
};
const validateContent = async (post: BlogPost): Promise<boolean> => {
await new Promise(resolve => setTimeout(resolve, 200));
return post.content.length > 100 && post.title.length > 5;
};
const analyzeSEO = async (post: BlogPost): Promise<number> => {
await new Promise(resolve => setTimeout(resolve, 300));
// Mock SEO score calculation
let score = 50;
if (post.tags.length > 0) score += 20;
if (post.title.length > 20) score += 15;
if (post.content.length > 1000) score += 15;
return Math.min(score, 100);
};
const savePublishedPost = async (post: BlogPost): Promise<void> => {
await new Promise(resolve => setTimeout(resolve, 100));
console.log('πΎ Post saved to database');
};
const postToSocialMedia = async (post: BlogPost): Promise<boolean> => {
await new Promise(resolve => setTimeout(resolve, 400));
console.log('π± Posted to Twitter and LinkedIn');
return true;
};
const sendNotifications = async (post: BlogPost, author: Author): Promise<string[]> => {
await new Promise(resolve => setTimeout(resolve, 200));
console.log('π§ Notifications sent');
return ['email_subscribers', 'push_notifications', 'slack_team'];
};
const generatePublicUrl = (post: BlogPost): string => {
const slug = post.title.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
return `https://blog.example.com/posts/${slug}`;
};
// π Usage example
publishBlogPost('post_123')
.then((result: PublishResult) => {
console.log('π Blog post published successfully!');
console.log('π SEO Score:', result.seoScore);
console.log('π Public URL:', result.publicUrl);
console.log('π’ Notifications:', result.notificationsSent.join(', '));
})
.catch((error: Error) => {
console.error('π₯ Publishing failed:', error.message);
});
π Conclusion
Congratulations! π Youβve mastered the art of promise chaining in TypeScript. You now have the skills to:
- β Chain promises for elegant sequential async operations
- π Transform data through complex multi-step pipelines
- π¨ Handle errors robustly with fallbacks and retry logic
- π Combine patterns mixing parallel and sequential operations
- ποΈ Build workflows that are maintainable and type-safe
Youβve learned to create sophisticated async sequences that handle real-world complexity while maintaining clean, readable code. Keep practicing these patterns, and youβll be the async flow master your team needs! π
π Next Steps
Ready to level up further? Check out these advanced topics:
- π Promise.all() for parallel operations and performance
- β‘ Async/Await syntax for even cleaner async code
- π RxJS Observables for reactive programming patterns
- π Stream Processing for handling large data flows
- π― Error Boundaries for bulletproof error handling