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
- ๐ฏ Layered Defense: Multiple error boundaries at different application levels
- ๐ Smart Retries: Exponential backoff with jitter and error classification
- ๐จ Circuit Breaking: Prevent cascade failures with circuit breaker patterns
- ๐ Comprehensive Monitoring: Track errors, metrics, and recovery patterns
- ๐ 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! โกโจ