retry-fallback

Retry & Fallback Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "retry-fallback" with this command: npx skills add dadbodgeoff/drift/dadbodgeoff-drift-retry-fallback

Retry & Fallback Patterns

Handle transient failures gracefully.

When to Use This Skill

  • Network requests that occasionally fail

  • External services with temporary outages

  • Need graceful degradation when dependencies fail

  • Want to avoid cascading failures

Core Concepts

  • Exponential backoff - Increasing delays between retries

  • Jitter - Random variation prevents thundering herd

  • Fallback - Alternative data source when primary fails

  • Graceful degradation - Reduced functionality beats total failure

TypeScript Implementation

Retry with Exponential Backoff

// retry.ts export interface RetryConfig { maxRetries: number; baseDelayMs: number; maxDelayMs: number; backoffMultiplier: number; jitter: boolean; retryableErrors?: (error: Error) => boolean; }

const DEFAULT_CONFIG: RetryConfig = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 30000, backoffMultiplier: 2, jitter: true, };

function calculateDelay(attempt: number, config: RetryConfig): number { let delay = config.baseDelayMs * Math.pow(config.backoffMultiplier, attempt); delay = Math.min(delay, config.maxDelayMs);

if (config.jitter) { const jitterRange = delay * 0.25; delay = delay + (Math.random() * jitterRange * 2 - jitterRange); }

return Math.floor(delay); }

function isRetryable(error: Error): boolean { const message = error.message.toLowerCase(); return ( message.includes('network') || message.includes('timeout') || message.includes('rate limit') || message.includes('429') || message.includes('503') || message.includes('502') ); }

export async function retry<T>( fn: () => Promise<T>, config: Partial<RetryConfig> = {} ): Promise<T> { const cfg = { ...DEFAULT_CONFIG, ...config }; const shouldRetry = cfg.retryableErrors || isRetryable;

let lastError: Error;

for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error));

  if (attempt === cfg.maxRetries || !shouldRetry(lastError)) {
    throw lastError;
  }
  
  const delay = calculateDelay(attempt, cfg);
  console.log(`[Retry] Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
  
  await new Promise(resolve => setTimeout(resolve, delay));
}

}

throw lastError!; }

Fallback Pattern

// fallback.ts export interface FallbackConfig<T> { timeout?: number; fallbackValue?: T; fallbackFn?: () => T | Promise<T>; onFallback?: (error: Error) => void; }

export async function withFallback<T>( fn: () => Promise<T>, config: FallbackConfig<T> ): Promise<T> { const { timeout, fallbackValue, fallbackFn, onFallback } = config;

try { if (timeout) { return await Promise.race([ fn(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout) ), ]); } return await fn(); } catch (error) { const err = error instanceof Error ? error : new Error(String(error));

if (onFallback) onFallback(err);

if (fallbackFn) return await fallbackFn();
if (fallbackValue !== undefined) return fallbackValue;

throw err;

} }

export async function tryMultiple<T>( sources: Array<() => Promise<T>>, options: { timeout?: number } = {} ): Promise<T> { const errors: Error[] = [];

for (const source of sources) { try { if (options.timeout) { return await Promise.race([ source(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), options.timeout) ), ]); } return await source(); } catch (error) { errors.push(error instanceof Error ? error : new Error(String(error))); } }

throw new AggregateError(errors, 'All sources failed'); }

Combined: Retry with Fallback

// resilient-fetch.ts export async function resilientFetch<T>( fn: () => Promise<T>, config: { retry?: Partial<RetryConfig>; fallback?: FallbackConfig<T>; } = {} ): Promise<T> { const withRetryFn = config.retry ? () => retry(fn, config.retry) : fn;

if (config.fallback) { return withFallback(withRetryFn, config.fallback); }

return withRetryFn(); }

Python Implementation

retry.py

import asyncio import random from typing import Callable, TypeVar, Optional from functools import wraps

T = TypeVar('T')

class RetryConfig: def init( self, max_retries: int = 3, base_delay: float = 1.0, max_delay: float = 30.0, backoff_multiplier: float = 2.0, jitter: bool = True, ): self.max_retries = max_retries self.base_delay = base_delay self.max_delay = max_delay self.backoff_multiplier = backoff_multiplier self.jitter = jitter

def calculate_delay(attempt: int, config: RetryConfig) -> float: delay = config.base_delay * (config.backoff_multiplier ** attempt) delay = min(delay, config.max_delay)

if config.jitter:
    jitter_range = delay * 0.25
    delay = delay + random.uniform(-jitter_range, jitter_range)

return delay

async def retry( fn: Callable[[], T], config: Optional[RetryConfig] = None, retryable: Optional[Callable[[Exception], bool]] = None, ) -> T: config = config or RetryConfig()

def is_retryable(error: Exception) -> bool:
    if retryable:
        return retryable(error)
    msg = str(error).lower()
    return any(x in msg for x in ['network', 'timeout', '429', '503'])

last_error: Exception = Exception("No attempts made")

for attempt in range(config.max_retries + 1):
    try:
        return await fn()
    except Exception as e:
        last_error = e
        
        if attempt == config.max_retries or not is_retryable(e):
            raise
        
        delay = calculate_delay(attempt, config)
        print(f"[Retry] Attempt {attempt + 1} failed, retrying in {delay:.1f}s")
        await asyncio.sleep(delay)

raise last_error

def with_retry(config: Optional[RetryConfig] = None): """Decorator for retry.""" def decorator(fn): @wraps(fn) async def wrapper(*args, **kwargs): return await retry(lambda: fn(*args, **kwargs), config) return wrapper return decorator

Fallback

async def with_fallback( fn: Callable[[], T], fallback: T | Callable[[], T], timeout: Optional[float] = None, ) -> T: try: if timeout: return await asyncio.wait_for(fn(), timeout=timeout) return await fn() except Exception as e: print(f"[Fallback] Primary failed: {e}") if callable(fallback): return await fallback() if asyncio.iscoroutinefunction(fallback) else fallback() return fallback

Usage Examples

Basic Retry

const data = await retry( () => fetch('https://api.example.com/data').then(r => r.json()), { maxRetries: 3 } );

Retry with Custom Logic

const result = await retry( () => processPayment(order), { maxRetries: 5, baseDelayMs: 2000, retryableErrors: (error) => error.message.includes('temporary'), } );

Fallback to Cache

const data = await withFallback( () => fetchFromAPI(), { timeout: 5000, fallbackFn: () => getFromCache(), onFallback: (error) => { console.warn('Using cached data:', error.message); }, } );

Try Multiple Sources

const user = await tryMultiple([ () => fetchFromPrimaryDB(userId), () => fetchFromReplicaDB(userId), () => fetchFromCache(userId), ], { timeout: 3000 });

Combined Pattern

const dashboard = await resilientFetch( () => fetchFromMLPipeline(), { retry: { maxRetries: 2, baseDelayMs: 500 }, fallback: { timeout: 5000, fallbackFn: async () => { const snapshot = await fetchLatestSnapshot(); return snapshot || { status: 'degraded', data: getCachedData() }; }, }, } );

Graceful Degradation

interface DegradedResponse<T> { data: T; degraded: boolean; message?: string; }

async function withDegradation<T>( fullFn: () => Promise<T>, degradedFn: () => Promise<T>, minimalFn: () => T ): Promise<DegradedResponse<T>> { try { return { data: await fullFn(), degraded: false }; } catch { try { return { data: await degradedFn(), degraded: true, message: 'Some features unavailable' }; } catch { return { data: minimalFn(), degraded: true, message: 'Limited functionality' }; } } }

// Usage const response = await withDegradation( () => fetchRealtimeAnalytics(), () => fetchCachedAnalytics(), () => ({ message: 'Analytics unavailable', data: [] }) );

Best Practices

  • Only retry transient errors - Don't retry 400 Bad Request

  • Use jitter - Prevents thundering herd

  • Set max delay - Don't wait forever

  • Log retries - Track retry frequency

  • Have fallbacks - Always have a backup plan

Common Mistakes

  • Retrying non-transient errors (400, 401, 404)

  • No jitter (all instances retry simultaneously)

  • Infinite retries (no max)

  • No fallback for critical paths

  • Not logging retry attempts

Related Skills

  • Circuit Breaker

  • Graceful Degradation

  • Caching Strategies

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

oauth-social-login

No summary provided by upstream source.

Repository SourceNeeds Review
General

sse-streaming

No summary provided by upstream source.

Repository SourceNeeds Review
General

multi-tenancy

No summary provided by upstream source.

Repository SourceNeeds Review