/**
* Comprehensive error handling with retry logic and fallbacks
*/
export interface RetryConfig {
maxAttempts: number;
baseDelay: number;
maxDelay: number;
backoffMultiplier: number;
jitter: boolean;
}
export interface ErrorContext {
operation: string;
attempt: number;
maxAttempts: number;
error: Error;
timestamp: Date;
metadata?: Record<string, any>;
}
export class ErrorHandler {
private static defaultRetryConfig: RetryConfig = {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 10000,
backoffMultiplier: 2,
jitter: true
};
/**
* Execute function with retry logic
*/
static async withRetry<T>(
operation: () => Promise<T>,
config: Partial<RetryConfig> = {},
context: string = 'unknown'
): Promise<T> {
const retryConfig = { ...this.defaultRetryConfig, ...config };
let lastError: Error;
for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
const errorContext: ErrorContext = {
operation: context,
attempt,
maxAttempts: retryConfig.maxAttempts,
error: lastError,
timestamp: new Date()
};
// Log error context
console.error(`Attempt ${attempt}/${retryConfig.maxAttempts} failed for ${context}:`, {
error: lastError.message,
stack: lastError.stack?.substring(0, 200) + '...'
});
// Don't retry on final attempt
if (attempt === retryConfig.maxAttempts) {
break;
}
// Check if error is retryable
if (!this.isRetryableError(lastError)) {
console.error(`Non-retryable error encountered: ${lastError.message}`);
break;
}
// Calculate delay with exponential backoff and jitter
const delay = this.calculateDelay(attempt, retryConfig);
console.log(`Retrying in ${delay}ms...`);
await this.sleep(delay);
}
}
throw this.enhanceError(lastError!, context, retryConfig.maxAttempts);
}
/**
* Execute function with fallback
*/
static async withFallback<T>(
primaryOperation: () => Promise<T>,
fallbackOperation: () => Promise<T>,
context: string = 'unknown'
): Promise<T> {
try {
return await primaryOperation();
} catch (error) {
console.warn(`Primary operation failed for ${context}, trying fallback:`, error);
try {
return await fallbackOperation();
} catch (fallbackError) {
throw this.enhanceError(
fallbackError as Error,
`${context} (fallback)`,
1,
{ originalError: error }
);
}
}
}
/**
* Execute function with timeout
*/
static async withTimeout<T>(
operation: () => Promise<T>,
timeoutMs: number,
context: string = 'unknown'
): Promise<T> {
return Promise.race([
operation(),
new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation '${context}' timed out after ${timeoutMs}ms`));
}, timeoutMs);
})
]);
}
/**
* Execute function with circuit breaker pattern
*/
static async withCircuitBreaker<T>(
operation: () => Promise<T>,
context: string = 'unknown',
failureThreshold: number = 5,
timeoutMs: number = 60000
): Promise<T> {
// Simple circuit breaker implementation
const circuitKey = `circuit_breaker_${context}`;
const circuitState = this.getCircuitState(circuitKey);
if (circuitState.state === 'OPEN') {
if (Date.now() - circuitState.lastFailure < timeoutMs) {
throw new Error(`Circuit breaker is OPEN for ${context}. Too many recent failures.`);
} else {
// Reset circuit to HALF_OPEN
circuitState.state = 'HALF_OPEN';
circuitState.failureCount = 0;
}
}
try {
const result = await operation();
// Success - reset circuit
if (circuitState.state === 'HALF_OPEN') {
circuitState.state = 'CLOSED';
}
circuitState.failureCount = 0;
return result;
} catch (error) {
circuitState.failureCount++;
circuitState.lastFailure = Date.now();
if (circuitState.failureCount >= failureThreshold) {
circuitState.state = 'OPEN';
}
throw error;
}
}
/**
* Check if error is retryable
*/
private static isRetryableError(error: Error): boolean {
const retryablePatterns = [
/timeout/i,
/network/i,
/connection/i,
/temporary/i,
/rate limit/i,
/too many requests/i,
/service unavailable/i,
/bad gateway/i,
/gateway timeout/i
];
const nonRetryablePatterns = [
/authentication/i,
/authorization/i,
/forbidden/i,
/not found/i,
/invalid/i,
/malformed/i
];
const errorMessage = error.message.toLowerCase();
// Check for non-retryable patterns first
for (const pattern of nonRetryablePatterns) {
if (pattern.test(errorMessage)) {
return false;
}
}
// Check for retryable patterns
for (const pattern of retryablePatterns) {
if (pattern.test(errorMessage)) {
return true;
}
}
// Default to retryable for unknown errors
return true;
}
/**
* Calculate delay with exponential backoff and jitter
*/
private static calculateDelay(attempt: number, config: RetryConfig): number {
let delay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt - 1);
delay = Math.min(delay, config.maxDelay);
if (config.jitter) {
// Add jitter to prevent thundering herd
delay = delay * (0.5 + Math.random() * 0.5);
}
return Math.floor(delay);
}
/**
* Sleep for specified milliseconds
*/
private static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Enhance error with context
*/
private static enhanceError(
error: Error,
context: string,
attempts: number,
metadata?: Record<string, any>
): Error {
const enhancedError = new Error(
`${context} failed after ${attempts} attempts: ${error.message}`
);
// Preserve original stack trace
enhancedError.stack = error.stack;
// Add metadata
(enhancedError as any).context = context;
(enhancedError as any).attempts = attempts;
(enhancedError as any).originalError = error;
(enhancedError as any).metadata = metadata;
(enhancedError as any).timestamp = new Date();
return enhancedError;
}
/**
* Get circuit breaker state
*/
private static getCircuitState(key: string): {
state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
failureCount: number;
lastFailure: number;
} {
const globalObj = globalThis as any;
if (!globalObj.circuitBreakers) {
globalObj.circuitBreakers = {};
}
if (!globalObj.circuitBreakers[key]) {
globalObj.circuitBreakers[key] = {
state: 'CLOSED',
failureCount: 0,
lastFailure: 0
};
}
return globalObj.circuitBreakers[key];
}
/**
* Handle authentication errors specifically
*/
static async handleAuthError<T>(
operation: () => Promise<T>,
context: string = 'authentication'
): Promise<T> {
try {
return await this.withRetry(operation, {
maxAttempts: 2,
baseDelay: 2000
}, context);
} catch (error) {
const authError = error as Error;
// Provide specific guidance for auth errors
if (authError.message.includes('credentials') ||
authError.message.includes('login') ||
authError.message.includes('authentication')) {
throw new Error(
`Authentication failed for ${context}. Please check your SUPERSTORE_EMAIL and SUPERSTORE_PASSWORD environment variables. Original error: ${authError.message}`
);
}
throw authError;
}
}
/**
* Handle network errors with specific retry logic
*/
static async handleNetworkError<T>(
operation: () => Promise<T>,
context: string = 'network'
): Promise<T> {
return this.withRetry(operation, {
maxAttempts: 5,
baseDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 1.5
}, context);
}
/**
* Handle scraping errors with fallback strategies
*/
static async handleScrapingError<T>(
primaryOperation: () => Promise<T>,
fallbackOperation: () => Promise<T>,
context: string = 'scraping'
): Promise<T> {
return this.withFallback(primaryOperation, fallbackOperation, context);
}
}