Prerequisites
- Basic TypeScript syntax and types π
- Understanding of Promises and async/await β‘
- Promise chaining and parallel operations π
What you'll learn
- Master try/catch patterns for bulletproof async error handling π‘οΈ
- Implement sophisticated error recovery and fallback strategies π
- Design resilient async flows with custom error types and boundaries π§
- Build production-ready error monitoring and logging systems π
π― Introduction
Welcome to the critical world of async error handling in TypeScript! π In this guide, weβll explore how to write bulletproof async code that gracefully handles errors and recovers from failures like a professional system.
Youβll discover how to transform fragile async operations into resilient, self-healing systems that provide excellent user experiences even when things go wrong. Whether youβre building APIs π, processing data streams π, or orchestrating complex workflows π, mastering async error handling is essential for creating production-ready TypeScript applications.
By the end of this tutorial, youβll be writing async code thatβs not just functional, but unbreakable and ready for the real world! π¨ Letβs dive in! πββοΈ
π Understanding Async Error Handling
π€ What Makes Async Errors Different?
Async errors are like hidden landmines in your code π£. Unlike synchronous errors that happen immediately and are easy to track, async errors can occur at any time, in any order, and can cascade through your application in unexpected ways.
In TypeScript async contexts, errors present unique challenges:
- β¨ Timing unpredictability - errors can happen at any point in the async flow
- π Cascade effects - one failed operation can break entire workflows
- π‘οΈ Context loss - error stack traces can be harder to follow
- π¦ Multiple failure modes - network, validation, business logic, and system errors
π‘ Why Master Async Error Handling?
Hereβs why robust async error handling is critical:
- User Experience π: Graceful degradation instead of crashes
- System Reliability π§: Services that recover from failures automatically
- Debugging Efficiency π: Clear error information for quick fixes
- Business Continuity πΌ: Operations continue even when components fail
- Monitoring & Alerting π: Proactive issue detection and resolution
Real-world example: When a payment service goes down, your e-commerce site should gracefully fall back to alternative payment methods rather than showing a blank error page! π³
π§ Basic Try/Catch Patterns
π Essential Try/Catch Structure
Letβs start with fundamental async error handling patterns:
// π― Basic try/catch with async/await
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
timestamp: Date;
}
// β
Proper async error handling structure
const fetchUserData = async (userId: string): Promise<ApiResponse<User>> => {
try {
console.log('π€ Fetching user data...');
// π Input validation first
if (!userId || userId.trim() === '') {
throw new Error('User ID is required');
}
// π API call that might fail
const userData = await callUserAPI(userId);
console.log('β
User data fetched successfully');
return {
success: true,
data: userData,
timestamp: new Date()
};
} catch (error) {
// π¨ Error handling with proper logging
console.error('π₯ Failed to fetch user data:', error.message);
return {
success: false,
error: error.message || 'Unknown error occurred',
timestamp: new Date()
};
}
};
// ποΈ Type definitions for clarity
interface User {
id: string;
name: string;
email: string;
status: 'active' | 'inactive';
}
// π οΈ Mock API that sometimes fails
const callUserAPI = async (userId: string): Promise<User> => {
await new Promise(resolve => setTimeout(resolve, 200));
// π² Simulate different types of failures
const randomFailure = Math.random();
if (randomFailure < 0.1) {
throw new Error('Network timeout - please try again');
}
if (randomFailure < 0.2) {
throw new Error('User service temporarily unavailable');
}
if (userId === 'invalid') {
throw new Error('User not found');
}
return {
id: userId,
name: 'John Doe',
email: '[email protected]',
status: 'active'
};
};
π Multiple Try/Catch Blocks for Granular Control
// π― Granular error handling for complex workflows
interface ProcessingResult {
step: string;
success: boolean;
data?: any;
error?: string;
duration: number;
}
interface WorkflowResult {
overallSuccess: boolean;
results: ProcessingResult[];
totalDuration: number;
failedSteps: string[];
}
// π Complex workflow with step-by-step error handling
const processUserWorkflow = async (userId: string): Promise<WorkflowResult> => {
const results: ProcessingResult[] = [];
const failedSteps: string[] = [];
const workflowStart = Date.now();
// π Step 1: Validate user input
let stepStart = Date.now();
try {
console.log('π Step 1: Validating user input...');
if (!userId || userId.length < 3) {
throw new Error('Invalid user ID format');
}
results.push({
step: 'validation',
success: true,
data: { userId },
duration: Date.now() - stepStart
});
console.log('β
Step 1 completed: Input validation successful');
} catch (error) {
console.error('β Step 1 failed:', error.message);
results.push({
step: 'validation',
success: false,
error: error.message,
duration: Date.now() - stepStart
});
failedSteps.push('validation');
// π Critical step failed - abort workflow
return {
overallSuccess: false,
results,
totalDuration: Date.now() - workflowStart,
failedSteps
};
}
// π€ Step 2: Fetch user data (optional - continue on failure)
stepStart = Date.now();
let userData: User | null = null;
try {
console.log('π€ Step 2: Fetching user data...');
userData = await callUserAPI(userId);
results.push({
step: 'user_fetch',
success: true,
data: userData,
duration: Date.now() - stepStart
});
console.log('β
Step 2 completed: User data fetched');
} catch (error) {
console.warn('β οΈ Step 2 failed (non-critical):', error.message);
results.push({
step: 'user_fetch',
success: false,
error: error.message,
duration: Date.now() - stepStart
});
failedSteps.push('user_fetch');
// Continue workflow - this step is optional
}
// π Step 3: Process user analytics
stepStart = Date.now();
try {
console.log('π Step 3: Processing user analytics...');
const analyticsData = await processUserAnalytics(userId, userData);
results.push({
step: 'analytics',
success: true,
data: analyticsData,
duration: Date.now() - stepStart
});
console.log('β
Step 3 completed: Analytics processed');
} catch (error) {
console.warn('β οΈ Step 3 failed (non-critical):', error.message);
results.push({
step: 'analytics',
success: false,
error: error.message,
duration: Date.now() - stepStart
});
failedSteps.push('analytics');
// Continue workflow - analytics is optional
}
// π§ Step 4: Send notifications
stepStart = Date.now();
try {
console.log('π§ Step 4: Sending notifications...');
const notificationResult = await sendUserNotifications(userId);
results.push({
step: 'notifications',
success: true,
data: notificationResult,
duration: Date.now() - stepStart
});
console.log('β
Step 4 completed: Notifications sent');
} catch (error) {
console.warn('β οΈ Step 4 failed (non-critical):', error.message);
results.push({
step: 'notifications',
success: false,
error: error.message,
duration: Date.now() - stepStart
});
failedSteps.push('notifications');
// Continue - notifications are optional
}
const totalDuration = Date.now() - workflowStart;
const overallSuccess = failedSteps.length === 0;
console.log(`π Workflow completed in ${totalDuration}ms. Success: ${overallSuccess}`);
return {
overallSuccess,
results,
totalDuration,
failedSteps
};
};
// π οΈ Mock workflow functions
const processUserAnalytics = async (userId: string, userData: User | null) => {
await new Promise(resolve => setTimeout(resolve, 150));
if (Math.random() < 0.15) { // 15% failure rate
throw new Error('Analytics service temporarily unavailable');
}
return {
userId,
sessionCount: Math.floor(Math.random() * 100),
lastActivity: new Date(),
hasUserData: userData !== null
};
};
const sendUserNotifications = async (userId: string) => {
await new Promise(resolve => setTimeout(resolve, 100));
if (Math.random() < 0.1) { // 10% failure rate
throw new Error('Notification service temporarily unavailable');
}
return {
emailSent: true,
pushSent: true,
smsRequired: false
};
};
π¨ Custom Error Types and Classification
π·οΈ Structured Error Hierarchy
// π― Custom error classes for better error categorization
abstract class BaseError extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
abstract readonly isRetryable: boolean;
public readonly timestamp: Date;
public readonly context?: Record<string, any>;
constructor(message: string, context?: Record<string, any>) {
super(message);
this.name = this.constructor.name;
this.timestamp = new Date();
this.context = context;
}
}
// π Network-related errors
class NetworkError extends BaseError {
readonly code = 'NETWORK_ERROR';
readonly statusCode = 503;
readonly isRetryable = true;
constructor(message: string, context?: Record<string, any>) {
super(`Network error: ${message}`, context);
}
}
// π Validation errors
class ValidationError extends BaseError {
readonly code = 'VALIDATION_ERROR';
readonly statusCode = 400;
readonly isRetryable = false;
public readonly errors: Array<{
field: string;
message: string;
value?: any;
}>;
constructor(errors: Array<{ field: string; message: string; value?: any }>) {
const message = `Validation failed: ${errors.map(e => e.message).join(', ')}`;
super(message);
this.errors = errors;
}
}
// π« Authorization errors
class AuthorizationError extends BaseError {
readonly code = 'AUTHORIZATION_ERROR';
readonly statusCode = 403;
readonly isRetryable = false;
constructor(message: string = 'Access denied') {
super(message);
}
}
// π Not found errors
class NotFoundError extends BaseError {
readonly code = 'NOT_FOUND_ERROR';
readonly statusCode = 404;
readonly isRetryable = false;
constructor(resource: string, id: string) {
super(`${resource} with id '${id}' not found`);
}
}
// βοΈ Business logic errors
class BusinessLogicError extends BaseError {
readonly code = 'BUSINESS_LOGIC_ERROR';
readonly statusCode = 422;
readonly isRetryable = false;
constructor(message: string, context?: Record<string, any>) {
super(`Business rule violation: ${message}`, context);
}
}
// π§ System errors
class SystemError extends BaseError {
readonly code = 'SYSTEM_ERROR';
readonly statusCode = 500;
readonly isRetryable = true;
constructor(message: string, context?: Record<string, any>) {
super(`System error: ${message}`, context);
}
}
// π― Smart error classification utility
class ErrorClassifier {
static classify(error: any): BaseError {
// π If already a custom error, return as-is
if (error instanceof BaseError) {
return error;
}
const message = error.message || 'Unknown error';
const lowerMessage = message.toLowerCase();
// π Network-related patterns
if (lowerMessage.includes('network') ||
lowerMessage.includes('timeout') ||
lowerMessage.includes('connection') ||
lowerMessage.includes('fetch')) {
return new NetworkError(message);
}
// π Validation patterns
if (lowerMessage.includes('validation') ||
lowerMessage.includes('invalid') ||
lowerMessage.includes('required')) {
return new ValidationError([{
field: 'unknown',
message: message
}]);
}
// π« Authorization patterns
if (lowerMessage.includes('unauthorized') ||
lowerMessage.includes('forbidden') ||
lowerMessage.includes('access denied')) {
return new AuthorizationError(message);
}
// π Not found patterns
if (lowerMessage.includes('not found') ||
lowerMessage.includes('does not exist')) {
return new NotFoundError('Resource', 'unknown');
}
// π§ Default to system error
return new SystemError(message);
}
}
// π‘οΈ Enhanced error handling with classification
const enhancedAsyncOperation = async (userId: string): Promise<ApiResponse<User>> => {
try {
console.log('π Starting enhanced async operation...');
// β
Input validation with custom errors
if (!userId) {
throw new ValidationError([{
field: 'userId',
message: 'User ID is required',
value: userId
}]);
}
if (userId.length < 3) {
throw new ValidationError([{
field: 'userId',
message: 'User ID must be at least 3 characters',
value: userId
}]);
}
// π Authorization check
const hasPermission = await checkUserPermission(userId);
if (!hasPermission) {
throw new AuthorizationError('User does not have permission to access this resource');
}
// π€ Fetch user data
const userData = await fetchUserWithRetry(userId);
console.log('β
Enhanced operation completed successfully');
return {
success: true,
data: userData,
timestamp: new Date()
};
} catch (error) {
// π― Classify and handle error appropriately
const classifiedError = ErrorClassifier.classify(error);
console.error(`π₯ ${classifiedError.code}: ${classifiedError.message}`);
// π Log error details for monitoring
logError(classifiedError, { operation: 'enhancedAsyncOperation', userId });
return {
success: false,
error: classifiedError.message,
timestamp: new Date()
};
}
};
// π οΈ Helper functions
const checkUserPermission = async (userId: string): Promise<boolean> => {
await new Promise(resolve => setTimeout(resolve, 50));
// π² Simulate permission check
if (userId === 'unauthorized') {
return false;
}
return true;
};
const fetchUserWithRetry = async (userId: string): Promise<User> => {
if (userId === 'notfound') {
throw new NotFoundError('User', userId);
}
// π² Simulate network issues
if (Math.random() < 0.2) {
throw new NetworkError('Unable to connect to user service');
}
await new Promise(resolve => setTimeout(resolve, 100));
return {
id: userId,
name: 'Jane Smith',
email: '[email protected]',
status: 'active'
};
};
const logError = (error: BaseError, context: Record<string, any>) => {
const logEntry = {
timestamp: error.timestamp,
code: error.code,
message: error.message,
statusCode: error.statusCode,
isRetryable: error.isRetryable,
context: { ...error.context, ...context }
};
console.error('π Error logged:', JSON.stringify(logEntry, null, 2));
};
π Retry Mechanisms and Exponential Backoff
β‘ Intelligent Retry Strategies
// π― Advanced retry mechanism with exponential backoff
interface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
jitterMs: number;
retryableErrorCodes: string[];
}
interface RetryResult<T> {
success: boolean;
data?: T;
error?: BaseError;
attempts: number;
totalDuration: number;
attemptDetails: Array<{
attempt: number;
success: boolean;
duration: number;
error?: string;
}>;
}
// π Smart retry utility with comprehensive tracking
class RetryManager {
private static defaultConfig: RetryConfig = {
maxAttempts: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
backoffMultiplier: 2,
jitterMs: 100,
retryableErrorCodes: ['NETWORK_ERROR', 'SYSTEM_ERROR']
};
static async execute<T>(
operation: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<RetryResult<T>> {
const finalConfig = { ...this.defaultConfig, ...config };
const attemptDetails: RetryResult<T>['attemptDetails'] = [];
const startTime = Date.now();
let lastError: BaseError;
for (let attempt = 1; attempt <= finalConfig.maxAttempts; attempt++) {
const attemptStart = Date.now();
try {
console.log(`π Retry attempt ${attempt}/${finalConfig.maxAttempts}`);
// π Execute the operation
const result = await operation();
const attemptDuration = Date.now() - attemptStart;
attemptDetails.push({
attempt,
success: true,
duration: attemptDuration
});
console.log(`β
Operation succeeded on attempt ${attempt}`);
return {
success: true,
data: result,
attempts: attempt,
totalDuration: Date.now() - startTime,
attemptDetails
};
} catch (error) {
const classifiedError = ErrorClassifier.classify(error);
lastError = classifiedError;
const attemptDuration = Date.now() - attemptStart;
attemptDetails.push({
attempt,
success: false,
duration: attemptDuration,
error: classifiedError.message
});
console.warn(`β Attempt ${attempt} failed: ${classifiedError.message}`);
// π Check if error is retryable
if (!finalConfig.retryableErrorCodes.includes(classifiedError.code)) {
console.error(`π Error is not retryable: ${classifiedError.code}`);
break;
}
// π Don't retry on last attempt
if (attempt === finalConfig.maxAttempts) {
console.error(`π Max attempts reached: ${finalConfig.maxAttempts}`);
break;
}
// β±οΈ Calculate delay with exponential backoff and jitter
const baseDelay = finalConfig.baseDelayMs * Math.pow(finalConfig.backoffMultiplier, attempt - 1);
const jitter = Math.random() * finalConfig.jitterMs;
const delay = Math.min(baseDelay + jitter, finalConfig.maxDelayMs);
console.log(`β³ Waiting ${Math.round(delay)}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return {
success: false,
error: lastError!,
attempts: finalConfig.maxAttempts,
totalDuration: Date.now() - startTime,
attemptDetails
};
}
}
// π― Retry-enabled API operations
const fetchUserWithIntelligentRetry = async (userId: string): Promise<User> => {
const retryResult = await RetryManager.execute(
() => callUserAPI(userId),
{
maxAttempts: 5,
baseDelayMs: 500,
maxDelayMs: 10000,
backoffMultiplier: 1.5,
jitterMs: 200
}
);
if (retryResult.success) {
console.log(`π User fetched successfully after ${retryResult.attempts} attempts`);
return retryResult.data!;
} else {
console.error(`π₯ Failed to fetch user after ${retryResult.attempts} attempts`);
throw retryResult.error!;
}
};
// π Parallel operations with retry
const fetchMultipleUsersWithRetry = async (userIds: string[]): Promise<{
successful: User[];
failed: Array<{ userId: string; error: BaseError }>;
}> => {
console.log(`π₯ Fetching ${userIds.length} users with retry...`);
const operations = userIds.map(async (userId) => {
try {
const user = await fetchUserWithIntelligentRetry(userId);
return { success: true, userId, user };
} catch (error) {
return { success: false, userId, error: ErrorClassifier.classify(error) };
}
});
const results = await Promise.allSettled(operations);
const successful: User[] = [];
const failed: Array<{ userId: string; error: BaseError }> = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
if (result.value.success) {
successful.push(result.value.user);
} else {
failed.push({
userId: result.value.userId,
error: result.value.error
});
}
} else {
failed.push({
userId: userIds[index],
error: new SystemError('Promise rejected unexpectedly')
});
}
});
console.log(`β
Successful: ${successful.length}, Failed: ${failed.length}`);
return { successful, failed };
};
π‘οΈ Error Boundaries and Circuit Breakers
π§ Advanced Resilience Patterns
// π― Circuit breaker pattern for preventing cascade failures
enum CircuitState {
CLOSED = 'CLOSED', // Normal operation
OPEN = 'OPEN', // Blocking requests
HALF_OPEN = 'HALF_OPEN' // Testing if service recovered
}
interface CircuitBreakerConfig {
failureThreshold: number; // Number of failures to open circuit
recoveryTimeMs: number; // Time to wait before half-open
successThreshold: number; // Successes needed to close circuit
timeWindowMs: number; // Time window for failure counting
}
interface CircuitBreakerMetrics {
state: CircuitState;
failures: number;
successes: number;
lastFailureTime?: Date;
lastSuccessTime?: Date;
totalRequests: number;
blockedRequests: number;
}
// π Circuit breaker implementation
class CircuitBreaker {
private state = CircuitState.CLOSED;
private failures = 0;
private successes = 0;
private lastFailureTime?: Date;
private lastSuccessTime?: Date;
private totalRequests = 0;
private blockedRequests = 0;
private recentFailures: Date[] = [];
constructor(
private config: CircuitBreakerConfig,
private name: string = 'default'
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
this.totalRequests++;
// π Check circuit state
if (this.state === CircuitState.OPEN) {
if (this.shouldAttemptRecovery()) {
console.log(`π Circuit breaker '${this.name}' moving to HALF_OPEN`);
this.state = CircuitState.HALF_OPEN;
} else {
this.blockedRequests++;
throw new SystemError(`Circuit breaker '${this.name}' is OPEN - service unavailable`);
}
}
try {
// π Execute operation
const result = await operation();
// β
Operation succeeded
this.onSuccess();
return result;
} catch (error) {
// β Operation failed
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.successes++;
this.lastSuccessTime = new Date();
if (this.state === CircuitState.HALF_OPEN) {
if (this.successes >= this.config.successThreshold) {
console.log(`β
Circuit breaker '${this.name}' closing after ${this.successes} successes`);
this.state = CircuitState.CLOSED;
this.reset();
}
} else if (this.state === CircuitState.CLOSED) {
// π§Ή Clean old failures in closed state
this.cleanOldFailures();
}
}
private onFailure(): void {
this.failures++;
this.lastFailureTime = new Date();
this.recentFailures.push(new Date());
// π§Ή Clean old failures
this.cleanOldFailures();
if (this.state === CircuitState.HALF_OPEN) {
console.log(`β Circuit breaker '${this.name}' opening due to failure in HALF_OPEN`);
this.state = CircuitState.OPEN;
} else if (this.state === CircuitState.CLOSED) {
if (this.recentFailures.length >= this.config.failureThreshold) {
console.log(`π₯ Circuit breaker '${this.name}' opening due to ${this.recentFailures.length} recent failures`);
this.state = CircuitState.OPEN;
}
}
}
private shouldAttemptRecovery(): boolean {
if (!this.lastFailureTime) return false;
const timeSinceLastFailure = Date.now() - this.lastFailureTime.getTime();
return timeSinceLastFailure >= this.config.recoveryTimeMs;
}
private cleanOldFailures(): void {
const cutoffTime = Date.now() - this.config.timeWindowMs;
this.recentFailures = this.recentFailures.filter(
failureTime => failureTime.getTime() > cutoffTime
);
}
private reset(): void {
this.failures = 0;
this.successes = 0;
this.recentFailures = [];
}
getMetrics(): CircuitBreakerMetrics {
return {
state: this.state,
failures: this.failures,
successes: this.successes,
lastFailureTime: this.lastFailureTime,
lastSuccessTime: this.lastSuccessTime,
totalRequests: this.totalRequests,
blockedRequests: this.blockedRequests
};
}
}
// π Service wrapper with circuit breaker
class ResilientUserService {
private circuitBreaker: CircuitBreaker;
constructor() {
this.circuitBreaker = new CircuitBreaker({
failureThreshold: 5, // Open after 5 failures
recoveryTimeMs: 30000, // Wait 30 seconds before retry
successThreshold: 3, // Close after 3 successes
timeWindowMs: 60000 // 1-minute failure window
}, 'user-service');
}
async getUser(userId: string): Promise<User> {
return this.circuitBreaker.execute(async () => {
console.log(`π€ Fetching user ${userId} through circuit breaker...`);
return await callUserAPI(userId);
});
}
async getUsers(userIds: string[]): Promise<{
users: User[];
errors: Array<{ userId: string; error: string }>;
}> {
const users: User[] = [];
const errors: Array<{ userId: string; error: string }> = [];
// π Process in batches to avoid overwhelming the circuit breaker
const batchSize = 3;
for (let i = 0; i < userIds.length; i += batchSize) {
const batch = userIds.slice(i, i + batchSize);
const batchPromises = batch.map(async (userId) => {
try {
const user = await this.getUser(userId);
users.push(user);
} catch (error) {
errors.push({
userId,
error: error.message
});
}
});
await Promise.allSettled(batchPromises);
// π€ Small delay between batches
if (i + batchSize < userIds.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return { users, errors };
}
getServiceMetrics(): CircuitBreakerMetrics {
return this.circuitBreaker.getMetrics();
}
}
π― Real-World Application: Payment Processing System
π³ Comprehensive Payment Error Handling
// π― Payment processing with robust error handling
interface PaymentRequest {
orderId: string;
amount: number;
currency: string;
paymentMethodId: string;
customerId: string;
}
interface PaymentResult {
success: boolean;
transactionId?: string;
status: 'pending' | 'completed' | 'failed' | 'cancelled';
amount?: number;
processingFee?: number;
error?: string;
retryable: boolean;
processingTime: number;
}
// π¨ Payment-specific errors
class PaymentError extends BaseError {
readonly code = 'PAYMENT_ERROR';
readonly statusCode = 402;
readonly isRetryable: boolean;
constructor(message: string, isRetryable: boolean = false, context?: Record<string, any>) {
super(`Payment failed: ${message}`, context);
this.isRetryable = isRetryable;
}
}
class InsufficientFundsError extends PaymentError {
constructor() {
super('Insufficient funds', false);
}
}
class PaymentMethodError extends PaymentError {
constructor(message: string) {
super(`Payment method error: ${message}`, false);
}
}
class PaymentGatewayError extends PaymentError {
constructor(message: string) {
super(`Gateway error: ${message}`, true);
}
}
// π³ Resilient payment processor
class PaymentProcessor {
private primaryGateway: CircuitBreaker;
private fallbackGateway: CircuitBreaker;
constructor() {
this.primaryGateway = new CircuitBreaker({
failureThreshold: 3,
recoveryTimeMs: 60000, // 1 minute
successThreshold: 2,
timeWindowMs: 300000 // 5 minutes
}, 'primary-payment-gateway');
this.fallbackGateway = new CircuitBreaker({
failureThreshold: 5,
recoveryTimeMs: 30000, // 30 seconds
successThreshold: 3,
timeWindowMs: 180000 // 3 minutes
}, 'fallback-payment-gateway');
}
async processPayment(request: PaymentRequest): Promise<PaymentResult> {
const startTime = Date.now();
try {
console.log(`π³ Processing payment for order ${request.orderId}...`);
// β
Step 1: Validate payment request
await this.validatePaymentRequest(request);
// π³ Step 2: Try primary gateway
try {
const result = await this.primaryGateway.execute(() =>
this.callPrimaryGateway(request)
);
console.log(`β
Payment processed via primary gateway: ${result.transactionId}`);
return {
...result,
processingTime: Date.now() - startTime
};
} catch (primaryError) {
console.warn('β οΈ Primary gateway failed, trying fallback...');
// π Step 3: Try fallback gateway
try {
const result = await this.fallbackGateway.execute(() =>
this.callFallbackGateway(request)
);
console.log(`β
Payment processed via fallback gateway: ${result.transactionId}`);
return {
...result,
processingTime: Date.now() - startTime
};
} catch (fallbackError) {
console.error('π₯ Both gateways failed');
// π¨ Both gateways failed - determine if retryable
const classifiedError = ErrorClassifier.classify(fallbackError);
const retryable = classifiedError.isRetryable;
return {
success: false,
status: 'failed',
error: `Payment processing failed: ${classifiedError.message}`,
retryable,
processingTime: Date.now() - startTime
};
}
}
} catch (error) {
// β Validation or other critical error
const classifiedError = ErrorClassifier.classify(error);
console.error(`π₯ Payment processing failed: ${classifiedError.message}`);
return {
success: false,
status: 'failed',
error: classifiedError.message,
retryable: classifiedError.isRetryable,
processingTime: Date.now() - startTime
};
}
}
// π Batch payment processing with error isolation
async processPaymentBatch(requests: PaymentRequest[]): Promise<{
successful: PaymentResult[];
failed: PaymentResult[];
summary: {
totalAmount: number;
successfulAmount: number;
failedAmount: number;
successRate: number;
};
}> {
console.log(`π¦ Processing batch of ${requests.length} payments...`);
const successful: PaymentResult[] = [];
const failed: PaymentResult[] = [];
// π Process payments in parallel with error isolation
const results = await Promise.allSettled(
requests.map(request => this.processPayment(request))
);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
if (result.value.success) {
successful.push(result.value);
} else {
failed.push(result.value);
}
} else {
// Promise was rejected (shouldn't happen with our error handling)
failed.push({
success: false,
status: 'failed',
error: 'Unexpected promise rejection',
retryable: false,
processingTime: 0
});
}
});
// π Calculate summary
const totalAmount = requests.reduce((sum, req) => sum + req.amount, 0);
const successfulAmount = successful.reduce((sum, result) => sum + (result.amount || 0), 0);
const failedAmount = totalAmount - successfulAmount;
const successRate = (successful.length / requests.length) * 100;
console.log(`π Batch complete: ${successful.length}/${requests.length} successful (${successRate.toFixed(1)}%)`);
return {
successful,
failed,
summary: {
totalAmount,
successfulAmount,
failedAmount,
successRate
}
};
}
private async validatePaymentRequest(request: PaymentRequest): Promise<void> {
const errors: Array<{ field: string; message: string }> = [];
if (!request.orderId) {
errors.push({ field: 'orderId', message: 'Order ID is required' });
}
if (!request.amount || request.amount <= 0) {
errors.push({ field: 'amount', message: 'Amount must be greater than 0' });
}
if (request.amount > 100000) { // $100k limit
errors.push({ field: 'amount', message: 'Amount exceeds maximum limit' });
}
if (!request.currency || request.currency.length !== 3) {
errors.push({ field: 'currency', message: 'Invalid currency code' });
}
if (!request.paymentMethodId) {
errors.push({ field: 'paymentMethodId', message: 'Payment method is required' });
}
if (!request.customerId) {
errors.push({ field: 'customerId', message: 'Customer ID is required' });
}
if (errors.length > 0) {
throw new ValidationError(errors);
}
}
private async callPrimaryGateway(request: PaymentRequest): Promise<PaymentResult> {
await new Promise(resolve => setTimeout(resolve, 200));
// π² Simulate various failure scenarios
const random = Math.random();
if (random < 0.1) {
throw new PaymentGatewayError('Gateway timeout');
}
if (random < 0.15) {
throw new InsufficientFundsError();
}
if (random < 0.2) {
throw new PaymentMethodError('Card expired');
}
return {
success: true,
transactionId: `txn_primary_${Date.now()}`,
status: 'completed',
amount: request.amount,
processingFee: request.amount * 0.029, // 2.9% fee
retryable: false,
processingTime: 0 // Will be set by caller
};
}
private async callFallbackGateway(request: PaymentRequest): Promise<PaymentResult> {
await new Promise(resolve => setTimeout(resolve, 300));
// π² Different failure patterns for fallback
const random = Math.random();
if (random < 0.05) {
throw new PaymentGatewayError('Fallback gateway overloaded');
}
if (random < 0.1) {
throw new PaymentMethodError('Payment method not supported');
}
return {
success: true,
transactionId: `txn_fallback_${Date.now()}`,
status: 'completed',
amount: request.amount,
processingFee: request.amount * 0.035, // 3.5% fee (higher for fallback)
retryable: false,
processingTime: 0 // Will be set by caller
};
}
getGatewayMetrics() {
return {
primary: this.primaryGateway.getMetrics(),
fallback: this.fallbackGateway.getMetrics()
};
}
}
// π Usage example
const demonstratePaymentProcessing = async () => {
const processor = new PaymentProcessor();
const paymentRequests: PaymentRequest[] = [
{
orderId: 'order_001',
amount: 99.99,
currency: 'USD',
paymentMethodId: 'pm_123',
customerId: 'cust_456'
},
{
orderId: 'order_002',
amount: 149.50,
currency: 'USD',
paymentMethodId: 'pm_789',
customerId: 'cust_012'
},
{
orderId: 'order_003',
amount: 299.00,
currency: 'USD',
paymentMethodId: 'pm_345',
customerId: 'cust_678'
}
];
try {
const batchResult = await processor.processPaymentBatch(paymentRequests);
console.log('π Payment batch results:');
console.log(`β
Successful: ${batchResult.successful.length}`);
console.log(`β Failed: ${batchResult.failed.length}`);
console.log(`π° Success rate: ${batchResult.summary.successRate.toFixed(1)}%`);
console.log(`π΅ Total processed: $${batchResult.summary.successfulAmount.toFixed(2)}`);
// π Gateway metrics
const metrics = processor.getGatewayMetrics();
console.log('π Gateway metrics:', metrics);
} catch (error) {
console.error('π₯ Batch processing failed:', error);
}
};
π Conclusion
Congratulations! π Youβve mastered the art of bulletproof async error handling in TypeScript. You now have the skills to:
- β Structure robust try/catch blocks for comprehensive error management
- π¨ Design custom error hierarchies for better error classification and handling
- π Implement intelligent retry mechanisms with exponential backoff and jitter
- π‘οΈ Build resilient systems with circuit breakers and error boundaries
- π³ Handle real-world scenarios like payment processing with multiple failure modes
Youβve learned to build async systems that donβt just work, but gracefully handle failures and recover automatically. Keep practicing these patterns, and youβll be the reliability engineer your team needs! π
π Next Steps
Ready to level up further? Check out these advanced topics:
- π Async Iterators for streaming data with error handling
- π‘ Event-Driven Error Handling with EventEmitters
- π Distributed System Patterns like bulkheads and timeouts
- π Error Monitoring with APM tools and alerting
- π Chaos Engineering for testing failure scenarios