+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 123 of 355

๐Ÿ›ก ๏ธ Async Error Boundaries: Handling Async Errors

Master advanced error handling patterns for asynchronous TypeScript code with custom error boundaries, resilient systems, and production-ready error recovery ๐Ÿš€

๐Ÿ’ŽAdvanced
28 min read

Prerequisites

  • Strong understanding of async/await and Promise patterns ๐Ÿ“
  • Experience with error handling and try/catch blocks โšก
  • Knowledge of TypeScript classes and generics ๐Ÿ’ป

What you'll learn

  • Master advanced async error handling patterns and strategies ๐ŸŽฏ
  • Build resilient error boundaries for complex async operations ๐Ÿ—๏ธ
  • Implement production-ready error recovery and retry mechanisms ๐Ÿ›
  • Create enterprise-grade error monitoring and reporting systems โœจ

๐ŸŽฏ Introduction

Welcome to the fortress of async error handling! ๐Ÿ›ก๏ธ If unhandled async errors are like unexpected storms that can sink your application, then error boundaries are your lighthouses - guiding your code safely through the treacherous waters of asynchronous operations!

Error boundaries for async code go far beyond simple try/catch blocks. They encompass sophisticated patterns for error propagation, recovery strategies, circuit breakers, retry mechanisms, and comprehensive monitoring. In TypeScript, we can build type-safe error handling systems that not only catch errors but intelligently respond to them.

By the end of this tutorial, youโ€™ll be a master of async error handling, capable of building rock-solid applications that gracefully handle failures, recover automatically, and provide excellent user experiences even when things go wrong. Letโ€™s build an unbreakable async fortress! โšก

๐Ÿ“š Understanding Async Error Boundaries

๐Ÿค” What Are Async Error Boundaries?

Async error boundaries are comprehensive error handling systems that wrap asynchronous operations, providing centralized error management, recovery strategies, and monitoring for complex async workflows.

// ๐ŸŒŸ Basic async error boundary concept
interface AsyncError {
  error: Error;
  operation: string;
  timestamp: Date;
  context?: Record<string, any>;
  retryCount?: number;
}

interface ErrorBoundaryOptions {
  maxRetries?: number;
  retryDelay?: number;
  fallbackValue?: any;
  onError?: (error: AsyncError) => void;
  shouldRetry?: (error: Error, retryCount: number) => boolean;
}

// ๐ŸŽฏ Basic async error boundary implementation
class AsyncErrorBoundary<T = any> {
  private options: Required<ErrorBoundaryOptions>;
  private errorHistory: AsyncError[] = [];

  constructor(options: ErrorBoundaryOptions = {}) {
    this.options = {
      maxRetries: options.maxRetries ?? 3,
      retryDelay: options.retryDelay ?? 1000,
      fallbackValue: options.fallbackValue ?? null,
      onError: options.onError ?? this.defaultErrorHandler,
      shouldRetry: options.shouldRetry ?? this.defaultShouldRetry,
    };
  }

  // ๐Ÿ›ก๏ธ Wrap async operation with error boundary
  async execute<R>(
    operation: () => Promise<R>,
    operationName: string = 'anonymous',
    context?: Record<string, any>
  ): Promise<R> {
    let retryCount = 0;
    let lastError: Error;

    while (retryCount <= this.options.maxRetries) {
      try {
        console.log(`๐Ÿš€ Executing ${operationName} (attempt ${retryCount + 1})`);
        const result = await operation();
        
        if (retryCount > 0) {
          console.log(`โœ… ${operationName} succeeded after ${retryCount} retries`);
        }
        
        return result;
        
      } catch (error) {
        lastError = error as Error;
        
        const asyncError: AsyncError = {
          error: lastError,
          operation: operationName,
          timestamp: new Date(),
          context,
          retryCount,
        };

        this.errorHistory.push(asyncError);
        this.options.onError(asyncError);

        // Check if we should retry
        if (
          retryCount < this.options.maxRetries &&
          this.options.shouldRetry(lastError, retryCount)
        ) {
          retryCount++;
          const delay = this.calculateRetryDelay(retryCount);
          console.log(`๐Ÿ”„ Retrying ${operationName} in ${delay}ms (attempt ${retryCount + 1})`);
          await this.sleep(delay);
          continue;
        }

        // Max retries reached or shouldn't retry
        console.error(`โŒ ${operationName} failed after ${retryCount + 1} attempts`);
        
        if (this.options.fallbackValue !== null) {
          console.log(`๐Ÿ›Ÿ Using fallback value for ${operationName}`);
          return this.options.fallbackValue;
        }

        throw lastError;
      }
    }

    throw lastError!;
  }

  // ๐Ÿ”ง Default error handler
  private defaultErrorHandler = (asyncError: AsyncError): void => {
    console.error(`๐Ÿšจ Error in ${asyncError.operation}:`, {
      error: asyncError.error.message,
      timestamp: asyncError.timestamp,
      retryCount: asyncError.retryCount,
      context: asyncError.context,
    });
  };

  // ๐Ÿค” Default retry logic
  private defaultShouldRetry = (error: Error, retryCount: number): boolean => {
    // Don't retry on specific error types
    if (error.name === 'ValidationError' || error.name === 'AuthError') {
      return false;
    }

    // Don't retry on 4xx HTTP errors (client errors)
    if (error.message.includes('400') || error.message.includes('401') || 
        error.message.includes('403') || error.message.includes('404')) {
      return false;
    }

    return true;
  };

  // โฐ Calculate retry delay with exponential backoff
  private calculateRetryDelay(retryCount: number): number {
    const baseDelay = this.options.retryDelay;
    const exponentialDelay = baseDelay * Math.pow(2, retryCount - 1);
    const jitter = Math.random() * 0.1 * exponentialDelay; // Add 10% jitter
    return Math.min(exponentialDelay + jitter, 30000); // Cap at 30 seconds
  }

  // ๐Ÿ˜ด Sleep utility
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // ๐Ÿ“Š Get error statistics
  getErrorStats() {
    const now = Date.now();
    const recentErrors = this.errorHistory.filter(
      err => now - err.timestamp.getTime() < 3600000 // Last hour
    );

    return {
      totalErrors: this.errorHistory.length,
      recentErrors: recentErrors.length,
      errorsByOperation: this.groupErrorsByOperation(),
      lastError: this.errorHistory[this.errorHistory.length - 1],
    };
  }

  // ๐Ÿ“ˆ Group errors by operation
  private groupErrorsByOperation() {
    return this.errorHistory.reduce((acc, error) => {
      acc[error.operation] = (acc[error.operation] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);
  }
}

// ๐ŸŽฎ Basic usage example
const basicUsageExample = async (): Promise<void> => {
  const errorBoundary = new AsyncErrorBoundary({
    maxRetries: 3,
    retryDelay: 1000,
    onError: (error) => console.log(`๐Ÿ“‹ Logging error: ${error.error.message}`),
  });

  try {
    // ๐ŸŒ Wrap a potentially failing API call
    const result = await errorBoundary.execute(
      async () => {
        const response = await fetch('https://api.example.com/data');
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        return response.json();
      },
      'fetchApiData',
      { endpoint: 'https://api.example.com/data' }
    );

    console.log('โœ… API call succeeded:', result);

  } catch (error) {
    console.error('๐Ÿ’ฅ Final error after retries:', error);
  }
};

๐Ÿ—๏ธ Advanced Error Boundary Patterns

๐ŸŽฏ Typed Error Boundaries

Letโ€™s create sophisticated error boundaries with strong typing and specific error handling strategies:

// ๐ŸŒŸ Advanced typed error boundary system
// Custom error types for better error handling
abstract class AppError extends Error {
  abstract readonly code: string;
  abstract readonly statusCode: number;
  abstract readonly retryable: boolean;
  
  constructor(message: string, public readonly context?: Record<string, any>) {
    super(message);
    this.name = this.constructor.name;
  }
}

class NetworkError extends AppError {
  readonly code = 'NETWORK_ERROR';
  readonly statusCode = 503;
  readonly retryable = true;
}

class ValidationError extends AppError {
  readonly code = 'VALIDATION_ERROR';
  readonly statusCode = 400;
  readonly retryable = false;
}

class AuthenticationError extends AppError {
  readonly code = 'AUTH_ERROR';
  readonly statusCode = 401;
  readonly retryable = false;
}

class RateLimitError extends AppError {
  readonly code = 'RATE_LIMIT_ERROR';
  readonly statusCode = 429;
  readonly retryable = true;
}

class TimeoutError extends AppError {
  readonly code = 'TIMEOUT_ERROR';
  readonly statusCode = 408;
  readonly retryable = true;
}

// ๐Ÿ“ฆ Enhanced error boundary configuration
interface TypedErrorBoundaryConfig<T> {
  maxRetries?: number;
  baseDelay?: number;
  maxDelay?: number;
  backoffMultiplier?: number;
  jitterEnabled?: boolean;
  fallbackValue?: T | (() => T | Promise<T>);
  circuitBreaker?: CircuitBreakerConfig;
  errorClassifiers?: ErrorClassifier[];
  onError?: (error: BoundaryError) => void | Promise<void>;
  onRetry?: (attempt: number, error: Error) => void;
  onSuccess?: (result: T, attempts: number) => void;
  timeout?: number;
}

interface BoundaryError {
  originalError: Error;
  operation: string;
  attempt: number;
  timestamp: Date;
  duration: number;
  context?: Record<string, any>;
}

interface CircuitBreakerConfig {
  failureThreshold: number;
  resetTimeout: number;
  monitoringWindow: number;
}

interface ErrorClassifier {
  test: (error: Error) => boolean;
  action: 'retry' | 'fail' | 'fallback';
  delay?: number;
}

// ๐ŸŽฏ Circuit breaker for preventing cascade failures
class CircuitBreaker {
  private failures = 0;
  private lastFailureTime = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';

  constructor(private config: CircuitBreakerConfig) {}

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime > this.config.resetTimeout) {
        this.state = 'half-open';
        console.log('๐Ÿ”„ Circuit breaker transitioning to half-open');
      } else {
        throw new Error('Circuit breaker is open - operation rejected');
      }
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess(): void {
    this.failures = 0;
    this.state = 'closed';
    console.log('โœ… Circuit breaker reset to closed state');
  }

  private onFailure(): void {
    this.failures++;
    this.lastFailureTime = Date.now();

    if (this.failures >= this.config.failureThreshold) {
      this.state = 'open';
      console.log('๐Ÿšจ Circuit breaker opened due to repeated failures');
    }
  }

  getState() {
    return {
      state: this.state,
      failures: this.failures,
      lastFailureTime: this.lastFailureTime,
    };
  }
}

// ๐Ÿ›ก๏ธ Advanced typed error boundary
class TypedAsyncErrorBoundary<T = any> {
  private circuitBreaker?: CircuitBreaker;
  private errorHistory: BoundaryError[] = [];
  private config: Required<Omit<TypedErrorBoundaryConfig<T>, 'circuitBreaker' | 'errorClassifiers'>> & {
    circuitBreaker?: CircuitBreaker;
    errorClassifiers: ErrorClassifier[];
  };

  constructor(config: TypedErrorBoundaryConfig<T> = {}) {
    this.config = {
      maxRetries: config.maxRetries ?? 3,
      baseDelay: config.baseDelay ?? 1000,
      maxDelay: config.maxDelay ?? 30000,
      backoffMultiplier: config.backoffMultiplier ?? 2,
      jitterEnabled: config.jitterEnabled ?? true,
      fallbackValue: config.fallbackValue,
      onError: config.onError ?? this.defaultErrorHandler,
      onRetry: config.onRetry ?? this.defaultRetryHandler,
      onSuccess: config.onSuccess ?? this.defaultSuccessHandler,
      timeout: config.timeout ?? 30000,
      errorClassifiers: config.errorClassifiers ?? this.getDefaultClassifiers(),
    };

    if (config.circuitBreaker) {
      this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
    }
  }

  // ๐ŸŽฏ Main execution method with comprehensive error handling
  async execute<R extends T>(
    operation: () => Promise<R>,
    operationName: string = 'operation',
    context?: Record<string, any>
  ): Promise<R> {
    const startTime = Date.now();
    let attempt = 0;
    let lastError: Error;

    while (attempt <= this.config.maxRetries) {
      try {
        attempt++;
        console.log(`๐Ÿš€ Executing ${operationName} (attempt ${attempt}/${this.config.maxRetries + 1})`);

        // Wrap with timeout
        const result = await this.executeWithTimeout(operation);

        // Circuit breaker execution
        if (this.circuitBreaker) {
          return await this.circuitBreaker.execute(() => Promise.resolve(result));
        }

        this.config.onSuccess(result, attempt);
        return result;

      } catch (error) {
        lastError = error as Error;
        const duration = Date.now() - startTime;

        const boundaryError: BoundaryError = {
          originalError: lastError,
          operation: operationName,
          attempt,
          timestamp: new Date(),
          duration,
          context,
        };

        this.errorHistory.push(boundaryError);
        await this.config.onError(boundaryError);

        // Classify error and determine action
        const action = this.classifyError(lastError);

        if (action === 'fail' || attempt > this.config.maxRetries) {
          break;
        }

        if (action === 'fallback') {
          return await this.getFallbackValue();
        }

        // action === 'retry'
        this.config.onRetry(attempt, lastError);
        const delay = this.calculateDelay(attempt);
        console.log(`๐Ÿ”„ Retrying ${operationName} in ${delay}ms`);
        await this.sleep(delay);
      }
    }

    // All retries exhausted, try fallback
    if (this.config.fallbackValue !== undefined) {
      console.log(`๐Ÿ›Ÿ Using fallback value for ${operationName}`);
      return await this.getFallbackValue();
    }

    throw lastError!;
  }

  // โฐ Execute operation with timeout
  private async executeWithTimeout<R>(operation: () => Promise<R>): Promise<R> {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new TimeoutError(`Operation timed out after ${this.config.timeout}ms`));
      }, this.config.timeout);

      operation()
        .then(resolve)
        .catch(reject)
        .finally(() => clearTimeout(timeoutId));
    });
  }

  // ๐Ÿท๏ธ Classify errors to determine retry strategy
  private classifyError(error: Error): 'retry' | 'fail' | 'fallback' {
    // Check custom classifiers first
    for (const classifier of this.config.errorClassifiers) {
      if (classifier.test(error)) {
        return classifier.action;
      }
    }

    // Check if it's our custom error types
    if (error instanceof AppError) {
      return error.retryable ? 'retry' : 'fail';
    }

    // Default classification
    return 'retry';
  }

  // ๐Ÿ”ง Default error classifiers
  private getDefaultClassifiers(): ErrorClassifier[] {
    return [
      {
        test: (error) => error.name === 'ValidationError',
        action: 'fail',
      },
      {
        test: (error) => error.name === 'AuthenticationError',
        action: 'fail',
      },
      {
        test: (error) => error.message.includes('404'),
        action: 'fail',
      },
      {
        test: (error) => error.message.includes('429'), // Rate limit
        action: 'retry',
        delay: 5000,
      },
      {
        test: (error) => error.name === 'TimeoutError',
        action: 'retry',
      },
      {
        test: (error) => error.name === 'NetworkError',
        action: 'retry',
      },
    ];
  }

  // ๐Ÿ’ผ Get fallback value
  private async getFallbackValue(): Promise<T> {
    if (typeof this.config.fallbackValue === 'function') {
      return await (this.config.fallbackValue as () => T | Promise<T>)();
    }
    return this.config.fallbackValue as T;
  }

  // โฐ Calculate retry delay with backoff and jitter
  private calculateDelay(attempt: number): number {
    let delay = this.config.baseDelay * Math.pow(this.config.backoffMultiplier, attempt - 1);
    
    if (this.config.jitterEnabled) {
      const jitter = delay * 0.1 * (Math.random() - 0.5); // ยฑ5% jitter
      delay += jitter;
    }

    return Math.min(delay, this.config.maxDelay);
  }

  // ๐Ÿ˜ด Sleep utility
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // ๐Ÿ”ง Default handlers
  private defaultErrorHandler = async (error: BoundaryError): Promise<void> => {
    console.error(`๐Ÿšจ Error in ${error.operation} (attempt ${error.attempt}):`, {
      error: error.originalError.message,
      duration: `${error.duration}ms`,
      timestamp: error.timestamp.toISOString(),
      context: error.context,
    });
  };

  private defaultRetryHandler = (attempt: number, error: Error): void => {
    console.log(`๐Ÿ”„ Retry ${attempt} due to: ${error.message}`);
  };

  private defaultSuccessHandler = (result: T, attempts: number): void => {
    if (attempts > 1) {
      console.log(`โœ… Operation succeeded after ${attempts} attempts`);
    }
  };

  // ๐Ÿ“Š Error analytics
  getAnalytics() {
    const now = Date.now();
    const hourAgo = now - 3600000;
    const recentErrors = this.errorHistory.filter(e => e.timestamp.getTime() > hourAgo);

    return {
      totalErrors: this.errorHistory.length,
      recentErrors: recentErrors.length,
      errorsByType: this.groupBy(this.errorHistory, e => e.originalError.name),
      errorsByOperation: this.groupBy(this.errorHistory, e => e.operation),
      averageAttempts: this.calculateAverageAttempts(),
      circuitBreakerState: this.circuitBreaker?.getState(),
      lastError: this.errorHistory[this.errorHistory.length - 1],
    };
  }

  private groupBy<K extends string | number>(
    array: BoundaryError[],
    keyFn: (item: BoundaryError) => K
  ): Record<K, number> {
    return array.reduce((acc, item) => {
      const key = keyFn(item);
      acc[key] = (acc[key] || 0) + 1;
      return acc;
    }, {} as Record<K, number>);
  }

  private calculateAverageAttempts(): number {
    if (this.errorHistory.length === 0) return 0;
    const totalAttempts = this.errorHistory.reduce((sum, error) => sum + error.attempt, 0);
    return totalAttempts / this.errorHistory.length;
  }
}

// ๐ŸŽฎ Advanced usage examples
const advancedUsageExample = async (): Promise<void> => {
  // ๐Ÿ—๏ธ Configure advanced error boundary
  const errorBoundary = new TypedAsyncErrorBoundary<any>({
    maxRetries: 5,
    baseDelay: 1000,
    maxDelay: 30000,
    backoffMultiplier: 1.5,
    jitterEnabled: true,
    timeout: 10000,
    circuitBreaker: {
      failureThreshold: 5,
      resetTimeout: 60000,
      monitoringWindow: 300000,
    },
    fallbackValue: async () => {
      console.log('๐Ÿ›Ÿ Executing fallback strategy');
      return { data: [], cached: true };
    },
    errorClassifiers: [
      {
        test: (error) => error.message.includes('ENOTFOUND'),
        action: 'retry',
      },
      {
        test: (error) => error.message.includes('Invalid token'),
        action: 'fail',
      },
    ],
    onError: async (error) => {
      // Send to monitoring service
      console.log('๐Ÿ“Š Sending error to monitoring service:', error);
    },
    onRetry: (attempt, error) => {
      console.log(`๐Ÿ”„ Retry attempt ${attempt}: ${error.message}`);
    },
    onSuccess: (result, attempts) => {
      if (attempts > 1) {
        console.log(`๐ŸŽ‰ Success after ${attempts} attempts`);
      }
    },
  });

  try {
    // ๐ŸŒ Complex API operation with error boundary
    const apiData = await errorBoundary.execute(
      async () => {
        console.log('๐Ÿ“ก Making API request...');
        
        const response = await fetch('https://api.example.com/data', {
          headers: {
            'Authorization': 'Bearer token',
            'Content-Type': 'application/json',
          },
        });

        if (!response.ok) {
          if (response.status === 401) {
            throw new AuthenticationError('Invalid authentication token');
          }
          if (response.status === 429) {
            throw new RateLimitError('Rate limit exceeded');
          }
          if (response.status >= 500) {
            throw new NetworkError(`Server error: ${response.status}`);
          }
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const data = await response.json();
        
        // Validate response data
        if (!data || typeof data !== 'object') {
          throw new ValidationError('Invalid response format');
        }

        return data;
      },
      'fetchApiData',
      { 
        endpoint: 'https://api.example.com/data',
        timestamp: new Date().toISOString(),
      }
    );

    console.log('โœ… API data received:', apiData);

  } catch (error) {
    console.error('๐Ÿ’ฅ Final error after all retries:', error);
  }

  // ๐Ÿ“Š Check error analytics
  const analytics = errorBoundary.getAnalytics();
  console.log('๐Ÿ“ˆ Error analytics:', analytics);
};

๐Ÿ”„ Async Operation Patterns

Letโ€™s explore patterns for handling complex async workflows with multiple error boundaries:

// ๐ŸŒŸ Complex async workflow management
interface WorkflowStep<T> {
  name: string;
  operation: () => Promise<T>;
  dependencies?: string[];
  timeout?: number;
  retries?: number;
  fallback?: () => Promise<T>;
  critical?: boolean;
}

interface WorkflowResult<T> {
  stepName: string;
  result?: T;
  error?: Error;
  attempts: number;
  duration: number;
  success: boolean;
}

// ๐ŸŽฏ Workflow executor with error boundaries
class AsyncWorkflowExecutor {
  private stepBoundaries = new Map<string, TypedAsyncErrorBoundary>();
  private results = new Map<string, WorkflowResult<any>>();

  constructor(private globalBoundary?: TypedAsyncErrorBoundary) {}

  // ๐Ÿ—๏ธ Execute workflow with error handling
  async executeWorkflow<T extends Record<string, any>>(
    steps: WorkflowStep<any>[],
    options: {
      parallel?: boolean;
      failFast?: boolean;
      continueOnNonCriticalFailure?: boolean;
    } = {}
  ): Promise<{
    results: T;
    errors: Error[];
    summary: {
      total: number;
      successful: number;
      failed: number;
      duration: number;
    };
  }> {
    const startTime = Date.now();
    const results = {} as T;
    const errors: Error[] = [];

    console.log(`๐Ÿš€ Starting workflow with ${steps.length} steps`);

    try {
      if (options.parallel) {
        await this.executeStepsInParallel(steps, results, errors, options);
      } else {
        await this.executeStepsSequentially(steps, results, errors, options);
      }
    } catch (error) {
      console.error('๐Ÿ’ฅ Workflow execution failed:', error);
      errors.push(error as Error);
    }

    const endTime = Date.now();
    const successful = Object.keys(results).length;
    const failed = errors.length;

    const summary = {
      total: steps.length,
      successful,
      failed,
      duration: endTime - startTime,
    };

    console.log(`๐Ÿ“Š Workflow completed:`, summary);

    return { results, errors, summary };
  }

  // โžก๏ธ Execute steps sequentially
  private async executeStepsSequentially<T>(
    steps: WorkflowStep<any>[],
    results: T,
    errors: Error[],
    options: any
  ): Promise<void> {
    for (const step of steps) {
      try {
        const result = await this.executeStep(step);
        (results as any)[step.name] = result.result;
        
        if (!result.success && step.critical && options.failFast) {
          throw new Error(`Critical step ${step.name} failed`);
        }
        
      } catch (error) {
        errors.push(error as Error);
        
        if (step.critical || !options.continueOnNonCriticalFailure) {
          throw error;
        }
        
        console.log(`โš ๏ธ Non-critical step ${step.name} failed, continuing...`);
      }
    }
  }

  // โšก Execute steps in parallel
  private async executeStepsInParallel<T>(
    steps: WorkflowStep<any>[],
    results: T,
    errors: Error[],
    options: any
  ): Promise<void> {
    const stepPromises = steps.map(async (step) => {
      try {
        const result = await this.executeStep(step);
        return { step, result, success: true };
      } catch (error) {
        return { step, error, success: false };
      }
    });

    const stepResults = await Promise.allSettled(stepPromises);

    for (const stepResult of stepResults) {
      if (stepResult.status === 'fulfilled') {
        const { step, result, error, success } = stepResult.value;
        
        if (success && result) {
          (results as any)[step.name] = result.result;
        } else if (error) {
          errors.push(error);
          
          if (step.critical && options.failFast) {
            throw new Error(`Critical step ${step.name} failed`);
          }
        }
      } else {
        errors.push(new Error(`Step execution promise rejected: ${stepResult.reason}`));
      }
    }
  }

  // ๐ŸŽฏ Execute individual step with error boundary
  private async executeStep<T>(step: WorkflowStep<T>): Promise<WorkflowResult<T>> {
    const stepBoundary = this.getStepBoundary(step);
    const startTime = Date.now();

    try {
      console.log(`๐Ÿ“‹ Executing step: ${step.name}`);
      
      const result = await stepBoundary.execute(
        step.operation,
        step.name,
        { stepConfig: step }
      );

      const duration = Date.now() - startTime;
      
      const stepResult: WorkflowResult<T> = {
        stepName: step.name,
        result,
        attempts: 1, // This would be tracked by the boundary
        duration,
        success: true,
      };

      this.results.set(step.name, stepResult);
      console.log(`โœ… Step ${step.name} completed in ${duration}ms`);
      
      return stepResult;

    } catch (error) {
      const duration = Date.now() - startTime;
      
      // Try fallback if available
      if (step.fallback) {
        console.log(`๐Ÿ›Ÿ Attempting fallback for step: ${step.name}`);
        try {
          const fallbackResult = await step.fallback();
          
          const stepResult: WorkflowResult<T> = {
            stepName: step.name,
            result: fallbackResult,
            attempts: 1,
            duration,
            success: true,
          };

          this.results.set(step.name, stepResult);
          return stepResult;
          
        } catch (fallbackError) {
          console.error(`โŒ Fallback failed for step ${step.name}:`, fallbackError);
        }
      }

      const stepResult: WorkflowResult<T> = {
        stepName: step.name,
        error: error as Error,
        attempts: 1,
        duration,
        success: false,
      };

      this.results.set(step.name, stepResult);
      throw error;
    }
  }

  // ๐Ÿ›ก๏ธ Get or create error boundary for step
  private getStepBoundary(step: WorkflowStep<any>): TypedAsyncErrorBoundary {
    if (!this.stepBoundaries.has(step.name)) {
      const boundary = new TypedAsyncErrorBoundary({
        maxRetries: step.retries ?? 2,
        timeout: step.timeout ?? 30000,
        onError: async (error) => {
          console.error(`๐Ÿšจ Step ${step.name} error:`, {
            error: error.originalError.message,
            attempt: error.attempt,
            duration: error.duration,
          });
        },
      });
      
      this.stepBoundaries.set(step.name, boundary);
    }
    
    return this.stepBoundaries.get(step.name)!;
  }

  // ๐Ÿ“Š Get workflow analytics
  getWorkflowAnalytics() {
    const allResults = Array.from(this.results.values());
    
    return {
      totalSteps: allResults.length,
      successfulSteps: allResults.filter(r => r.success).length,
      failedSteps: allResults.filter(r => !r.success).length,
      averageDuration: allResults.reduce((sum, r) => sum + r.duration, 0) / allResults.length,
      stepResults: Object.fromEntries(this.results),
      errorSummary: allResults
        .filter(r => r.error)
        .map(r => ({ step: r.stepName, error: r.error!.message })),
    };
  }
}

// ๐ŸŽฎ Complex workflow example
const workflowExample = async (): Promise<void> => {
  const workflowExecutor = new AsyncWorkflowExecutor();

  // ๐Ÿ“‹ Define workflow steps
  const steps: WorkflowStep<any>[] = [
    {
      name: 'authenticate',
      operation: async () => {
        console.log('๐Ÿ” Authenticating user...');
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { token: 'auth-token-123', userId: 'user-456' };
      },
      critical: true,
      retries: 3,
    },
    {
      name: 'fetchUserData',
      operation: async () => {
        console.log('๐Ÿ‘ค Fetching user data...');
        await new Promise(resolve => setTimeout(resolve, 800));
        return { name: 'John Doe', email: '[email protected]' };
      },
      dependencies: ['authenticate'],
      retries: 2,
      fallback: async () => {
        console.log('๐Ÿ›Ÿ Using cached user data');
        return { name: 'Cached User', email: '[email protected]' };
      },
    },
    {
      name: 'fetchPreferences',
      operation: async () => {
        console.log('โš™๏ธ Fetching user preferences...');
        await new Promise(resolve => setTimeout(resolve, 600));
        return { theme: 'dark', language: 'en' };
      },
      dependencies: ['authenticate'],
      retries: 1,
      fallback: async () => {
        return { theme: 'light', language: 'en' };
      },
    },
    {
      name: 'fetchNotifications',
      operation: async () => {
        console.log('๐Ÿ”” Fetching notifications...');
        await new Promise(resolve => setTimeout(resolve, 400));
        
        // Simulate occasional failure
        if (Math.random() < 0.3) {
          throw new Error('Notification service unavailable');
        }
        
        return { count: 5, messages: ['New message', 'Update available'] };
      },
      dependencies: ['authenticate'],
      critical: false,
      retries: 2,
      fallback: async () => {
        return { count: 0, messages: [] };
      },
    },
    {
      name: 'generateDashboard',
      operation: async () => {
        console.log('๐Ÿ“Š Generating dashboard...');
        await new Promise(resolve => setTimeout(resolve, 1200));
        return { widgets: ['weather', 'calendar', 'tasks'], layout: 'grid' };
      },
      dependencies: ['fetchUserData', 'fetchPreferences'],
      critical: true,
    },
  ];

  try {
    // ๐Ÿš€ Execute workflow
    const { results, errors, summary } = await workflowExecutor.executeWorkflow(
      steps,
      {
        parallel: false, // Sequential execution
        failFast: false, // Continue on non-critical failures
        continueOnNonCriticalFailure: true,
      }
    );

    console.log('๐ŸŽ‰ Workflow results:', results);
    console.log('๐Ÿ“ˆ Workflow summary:', summary);

    if (errors.length > 0) {
      console.log('โš ๏ธ Workflow errors:', errors);
    }

    // ๐Ÿ“Š Get detailed analytics
    const analytics = workflowExecutor.getWorkflowAnalytics();
    console.log('๐Ÿ“Š Workflow analytics:', analytics);

  } catch (error) {
    console.error('๐Ÿ’ฅ Workflow failed completely:', error);
  }
};

๐Ÿ”ฅ Production-Ready Error Monitoring

๐Ÿ“Š Comprehensive Error Monitoring System

Letโ€™s build a production-ready error monitoring and alerting system:

// ๐ŸŒŸ Enterprise error monitoring and alerting system
interface ErrorMetrics {
  errorRate: number;
  averageResponseTime: number;
  p95ResponseTime: number;
  totalRequests: number;
  failedRequests: number;
  timeWindow: number;
}

interface AlertConfig {
  name: string;
  condition: (metrics: ErrorMetrics) => boolean;
  severity: 'low' | 'medium' | 'high' | 'critical';
  cooldown: number; // Minimum time between alerts
  channels: string[]; // slack, email, pagerduty, etc.
}

interface ErrorEvent {
  id: string;
  timestamp: Date;
  operation: string;
  error: Error;
  context: Record<string, any>;
  severity: 'low' | 'medium' | 'high' | 'critical';
  resolved: boolean;
  tags: string[];
}

// ๐ŸŽฏ Advanced error monitoring system
class ErrorMonitoringSystem {
  private events: ErrorEvent[] = [];
  private metrics: Map<string, ErrorMetrics> = new Map();
  private alerts: AlertConfig[] = [];
  private lastAlertTime: Map<string, number> = new Map();
  private metricsWindow = 5 * 60 * 1000; // 5 minutes
  
  constructor(
    private config: {
      maxEvents?: number;
      metricsRetention?: number;
      alertHandlers?: Map<string, (alert: AlertConfig, metrics: ErrorMetrics) => Promise<void>>;
    } = {}
  ) {
    this.config = {
      maxEvents: config.maxEvents ?? 10000,
      metricsRetention: config.metricsRetention ?? 24 * 60 * 60 * 1000, // 24 hours
      alertHandlers: config.alertHandlers ?? new Map(),
    };

    this.setupDefaultAlerts();
    this.startMetricsCollection();
  }

  // ๐Ÿ“‹ Register error event
  recordError(
    operation: string,
    error: Error,
    context: Record<string, any> = {},
    severity: 'low' | 'medium' | 'high' | 'critical' = 'medium'
  ): void {
    const event: ErrorEvent = {
      id: this.generateId(),
      timestamp: new Date(),
      operation,
      error,
      context,
      severity,
      resolved: false,
      tags: this.generateTags(error, context),
    };

    this.events.push(event);
    this.trimEvents();
    this.updateMetrics(operation);
    this.checkAlerts(operation);

    console.error(`๐Ÿ“Š Error recorded:`, {
      id: event.id,
      operation,
      error: error.message,
      severity,
      tags: event.tags,
    });
  }

  // ๐Ÿท๏ธ Generate tags for error categorization
  private generateTags(error: Error, context: Record<string, any>): string[] {
    const tags: string[] = [];

    // Error type tags
    tags.push(`error:${error.name}`);
    
    // HTTP status tags
    if (error.message.match(/\d{3}/)) {
      const statusMatch = error.message.match(/(\d{3})/);
      if (statusMatch) {
        const status = parseInt(statusMatch[1]);
        tags.push(`http:${status}`);
        
        if (status >= 400 && status < 500) tags.push('client-error');
        if (status >= 500) tags.push('server-error');
      }
    }

    // Context tags
    if (context.userId) tags.push(`user:${context.userId}`);
    if (context.endpoint) tags.push(`endpoint:${context.endpoint}`);
    if (context.method) tags.push(`method:${context.method}`);

    // Error pattern tags
    if (error.message.includes('timeout')) tags.push('timeout');
    if (error.message.includes('network')) tags.push('network');
    if (error.message.includes('auth')) tags.push('authentication');
    if (error.message.includes('permission')) tags.push('authorization');

    return tags;
  }

  // ๐Ÿ“ˆ Update metrics for operation
  private updateMetrics(operation: string): void {
    const now = Date.now();
    const windowStart = now - this.metricsWindow;
    
    const recentEvents = this.events.filter(
      event => event.operation === operation && 
               event.timestamp.getTime() >= windowStart
    );

    const totalRequests = recentEvents.length;
    const failedRequests = recentEvents.filter(event => !event.resolved).length;
    const errorRate = totalRequests > 0 ? (failedRequests / totalRequests) * 100 : 0;

    // Calculate response times (mock data for example)
    const responseTimes = recentEvents.map(() => Math.random() * 2000 + 100); // 100-2100ms
    const averageResponseTime = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length || 0;
    const p95ResponseTime = this.calculatePercentile(responseTimes, 0.95);

    const metrics: ErrorMetrics = {
      errorRate,
      averageResponseTime,
      p95ResponseTime,
      totalRequests,
      failedRequests,
      timeWindow: this.metricsWindow,
    };

    this.metrics.set(operation, metrics);
  }

  // ๐Ÿ“Š Calculate percentile
  private calculatePercentile(values: number[], percentile: number): number {
    if (values.length === 0) return 0;
    
    const sorted = values.sort((a, b) => a - b);
    const index = Math.ceil(sorted.length * percentile) - 1;
    return sorted[index] || 0;
  }

  // ๐Ÿšจ Check and trigger alerts
  private checkAlerts(operation: string): void {
    const metrics = this.metrics.get(operation);
    if (!metrics) return;

    for (const alert of this.alerts) {
      const lastAlertTime = this.lastAlertTime.get(alert.name) || 0;
      const now = Date.now();

      // Check cooldown
      if (now - lastAlertTime < alert.cooldown) {
        continue;
      }

      // Check condition
      if (alert.condition(metrics)) {
        this.triggerAlert(alert, metrics, operation);
        this.lastAlertTime.set(alert.name, now);
      }
    }
  }

  // ๐Ÿ”” Trigger alert
  private async triggerAlert(
    alert: AlertConfig, 
    metrics: ErrorMetrics, 
    operation: string
  ): Promise<void> {
    console.warn(`๐Ÿšจ ALERT: ${alert.name} triggered for operation: ${operation}`, {
      severity: alert.severity,
      metrics,
      channels: alert.channels,
    });

    // Send to configured channels
    for (const channel of alert.channels) {
      const handler = this.config.alertHandlers?.get(channel);
      if (handler) {
        try {
          await handler(alert, metrics);
        } catch (error) {
          console.error(`Failed to send alert to ${channel}:`, error);
        }
      }
    }
  }

  // โš™๏ธ Setup default alert configurations
  private setupDefaultAlerts(): void {
    this.alerts = [
      {
        name: 'High Error Rate',
        condition: (metrics) => metrics.errorRate > 50,
        severity: 'high',
        cooldown: 5 * 60 * 1000, // 5 minutes
        channels: ['slack', 'email'],
      },
      {
        name: 'Critical Error Rate',
        condition: (metrics) => metrics.errorRate > 80,
        severity: 'critical',
        cooldown: 2 * 60 * 1000, // 2 minutes
        channels: ['slack', 'email', 'pagerduty'],
      },
      {
        name: 'High Response Time',
        condition: (metrics) => metrics.p95ResponseTime > 5000,
        severity: 'medium',
        cooldown: 10 * 60 * 1000, // 10 minutes
        channels: ['slack'],
      },
      {
        name: 'Service Degradation',
        condition: (metrics) => metrics.errorRate > 20 && metrics.averageResponseTime > 2000,
        severity: 'high',
        cooldown: 5 * 60 * 1000,
        channels: ['slack', 'email'],
      },
    ];
  }

  // โฐ Start metrics collection
  private startMetricsCollection(): void {
    setInterval(() => {
      this.cleanupOldEvents();
      this.calculateGlobalMetrics();
    }, 60000); // Every minute
  }

  // ๐Ÿงน Cleanup old events
  private cleanupOldEvents(): void {
    const retentionTime = Date.now() - this.config.metricsRetention!;
    this.events = this.events.filter(event => event.timestamp.getTime() > retentionTime);
  }

  // ๐Ÿ“Š Calculate global metrics
  private calculateGlobalMetrics(): void {
    const operations = new Set(this.events.map(event => event.operation));
    
    for (const operation of operations) {
      this.updateMetrics(operation);
    }
  }

  // ๐Ÿ”ง Utility methods
  private generateId(): string {
    return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  private trimEvents(): void {
    if (this.events.length > this.config.maxEvents!) {
      this.events = this.events.slice(-this.config.maxEvents!);
    }
  }

  // ๐Ÿ“Š Public API methods
  getMetrics(operation?: string): Map<string, ErrorMetrics> | ErrorMetrics | undefined {
    if (operation) {
      return this.metrics.get(operation);
    }
    return this.metrics;
  }

  getRecentErrors(limit: number = 100): ErrorEvent[] {
    return this.events
      .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
      .slice(0, limit);
  }

  getErrorsByTag(tag: string): ErrorEvent[] {
    return this.events.filter(event => event.tags.includes(tag));
  }

  getErrorSummary() {
    const now = Date.now();
    const hourAgo = now - 3600000;
    const dayAgo = now - 86400000;

    const recentHour = this.events.filter(e => e.timestamp.getTime() > hourAgo);
    const recentDay = this.events.filter(e => e.timestamp.getTime() > dayAgo);

    return {
      total: this.events.length,
      lastHour: recentHour.length,
      lastDay: recentDay.length,
      byOperation: this.groupBy(this.events, e => e.operation),
      bySeverity: this.groupBy(this.events, e => e.severity),
      byErrorType: this.groupBy(this.events, e => e.error.name),
      topErrors: this.getTopErrors(10),
    };
  }

  private groupBy<T, K extends string | number>(
    array: T[],
    keyFn: (item: T) => K
  ): Record<K, number> {
    return array.reduce((acc, item) => {
      const key = keyFn(item);
      acc[key] = (acc[key] || 0) + 1;
      return acc;
    }, {} as Record<K, number>);
  }

  private getTopErrors(limit: number): Array<{ error: string; count: number }> {
    const errorCounts = this.groupBy(this.events, e => e.error.message);
    
    return Object.entries(errorCounts)
      .sort(([, a], [, b]) => b - a)
      .slice(0, limit)
      .map(([error, count]) => ({ error, count }));
  }
}

// ๐ŸŽฎ Production monitoring example
const productionMonitoringExample = async (): Promise<void> => {
  // ๐Ÿ—๏ธ Setup monitoring system with alert handlers
  const monitoring = new ErrorMonitoringSystem({
    maxEvents: 50000,
    alertHandlers: new Map([
      ['slack', async (alert, metrics) => {
        console.log(`๐Ÿ“ข Slack Alert: ${alert.name}`, { alert, metrics });
        // Send to Slack webhook
      }],
      ['email', async (alert, metrics) => {
        console.log(`๐Ÿ“ง Email Alert: ${alert.name}`, { alert, metrics });
        // Send email notification
      }],
      ['pagerduty', async (alert, metrics) => {
        console.log(`๐Ÿ“Ÿ PagerDuty Alert: ${alert.name}`, { alert, metrics });
        // Create PagerDuty incident
      }],
    ]),
  });

  // ๐Ÿ›ก๏ธ Enhanced error boundary with monitoring integration
  const monitoredBoundary = new TypedAsyncErrorBoundary({
    maxRetries: 3,
    onError: async (error) => {
      // Record error in monitoring system
      monitoring.recordError(
        error.operation,
        error.originalError,
        {
          attempt: error.attempt,
          duration: error.duration,
          context: error.context,
        },
        error.attempt === 1 ? 'medium' : 'high' // Escalate severity on retries
      );
    },
  });

  // ๐ŸŽฏ Simulate various operations with different error patterns
  const operations = [
    'user-authentication',
    'data-processing',
    'file-upload',
    'payment-processing',
    'notification-service',
  ];

  // ๐Ÿ“Š Simulate production traffic with errors
  for (let i = 0; i < 100; i++) {
    const operation = operations[Math.floor(Math.random() * operations.length)];
    
    try {
      await monitoredBoundary.execute(
        async () => {
          // Simulate occasional failures
          if (Math.random() < 0.15) { // 15% failure rate
            const errorTypes = [
              () => new NetworkError('Connection timeout'),
              () => new ValidationError('Invalid input data'),
              () => new AuthenticationError('Token expired'),
              () => new RateLimitError('Too many requests'),
              () => new Error('Unexpected server error'),
            ];
            
            const errorType = errorTypes[Math.floor(Math.random() * errorTypes.length)];
            throw errorType();
          }
          
          // Simulate processing time
          await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
          return { success: true, data: `Result for ${operation}` };
        },
        operation,
        { operationId: `op_${i}` }
      );
      
    } catch (error) {
      // Final error after retries - already recorded by monitoring
    }
    
    // Small delay between operations
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  // ๐Ÿ“Š Display monitoring results
  console.log('\n๐Ÿ“ˆ Error Monitoring Summary:');
  const summary = monitoring.getErrorSummary();
  console.log(summary);

  console.log('\n๐Ÿ“Š Current Metrics:');
  const metrics = monitoring.getMetrics();
  if (metrics instanceof Map) {
    for (const [operation, operationMetrics] of metrics) {
      console.log(`Operation: ${operation}`, operationMetrics);
    }
  }

  console.log('\n๐Ÿ”ฅ Recent Errors:');
  const recentErrors = monitoring.getRecentErrors(5);
  for (const error of recentErrors) {
    console.log(`${error.timestamp.toISOString()} - ${error.operation}: ${error.error.message}`);
  }
};

๐ŸŽ‰ Conclusion

Congratulations! Youโ€™ve mastered the fortress of async error handling! ๐Ÿ›ก๏ธ

๐ŸŽฏ What Youโ€™ve Learned

  • ๐Ÿ”ง Error Boundary Fundamentals: Building comprehensive async error handling systems
  • ๐ŸŽฏ Advanced Patterns: Typed error boundaries, circuit breakers, and retry strategies
  • ๐Ÿ”„ Workflow Management: Complex async operation orchestration with error recovery
  • ๐Ÿ“Š Production Monitoring: Enterprise-grade error tracking and alerting systems
  • ๐Ÿ› ๏ธ Resilience Patterns: Building applications that gracefully handle failures

๐Ÿš€ Key Benefits

  • ๐Ÿ›ก๏ธ Resilient Applications: Systems that recover gracefully from failures
  • ๐ŸŽฏ Type Safety: Comprehensive error handling with TypeScript type safety
  • ๐Ÿ“Š Observability: Deep insights into error patterns and system health
  • โšก Performance: Optimized retry strategies and circuit breakers
  • ๐Ÿ”ง Maintainability: Centralized error handling and recovery strategies

๐Ÿ”ฅ Best Practices Recap

  1. ๐ŸŽฏ Layered Defense: Multiple error boundaries at different application levels
  2. ๐Ÿ”„ Smart Retries: Exponential backoff with jitter and error classification
  3. ๐Ÿšจ Circuit Breaking: Prevent cascade failures with circuit breaker patterns
  4. ๐Ÿ“Š Comprehensive Monitoring: Track errors, metrics, and recovery patterns
  5. ๐Ÿ›Ÿ Graceful Degradation: Fallback strategies and user experience preservation

Youโ€™re now equipped to build bulletproof async applications that handle errors like a pro, recover intelligently, and provide exceptional user experiences even when things go wrong! ๐ŸŒŸ

Happy coding, and may your async operations never crash! โšกโœจ