Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/visible/cruel/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The Retry pattern automatically retries failed operations with configurable delays and backoff strategies. This helps handle transient failures that may succeed on subsequent attempts.

When to Use

  • Network requests that may fail due to temporary issues
  • Rate-limited APIs that return 429 errors
  • Database operations that may fail due to transient locks
  • Any operation where temporary failures are expected

API Reference

Function Signature

function withRetry<T extends AnyFn>(fn: T, options: RetryOptions): T

Options

attempts
number
required
Maximum number of retry attempts. Must be at least 1. This is the total number of attempts including the initial try.
delay
number | [number, number]
default:"1000"
Base delay in milliseconds between retries. Can be a single number or a range [min, max] for random delays.
backoff
'fixed' | 'linear' | 'exponential'
Backoff strategy for increasing delays:
  • fixed: Use the same delay for all retries
  • linear: Multiply delay by attempt number (delay × attempt)
  • exponential: Exponentially increase delay (delay × 2^(attempt-1))
maxDelay
number
Maximum delay in milliseconds. Caps the delay calculated by backoff strategies.
onRetry
(attempt: number, error: Error) => void
Callback function executed before each retry attempt. Receives the attempt number (1-indexed) and the error that caused the retry.
retryIf
(error: Error) => boolean
Predicate function to determine if an error should trigger a retry. If it returns false, the error is thrown immediately without retrying.

Examples

Basic Retry

import { withRetry } from 'cruel'

const fetchUser = async (id: string) => {
  const response = await fetch(`https://api.example.com/users/${id}`)
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
}

const resilientFetch = withRetry(fetchUser, {
  attempts: 3,
  delay: 1000,  // 1 second between retries
})

// Will retry up to 3 times with 1 second delay
const user = await resilientFetch('123')

Exponential Backoff

const apiCall = withRetry(fetchData, {
  attempts: 5,
  delay: 1000,           // Base delay: 1s
  backoff: 'exponential', // 1s, 2s, 4s, 8s, 16s
  maxDelay: 10000,       // Cap at 10 seconds
})

Linear Backoff

const apiCall = withRetry(fetchData, {
  attempts: 4,
  delay: 500,          // Base delay: 500ms
  backoff: 'linear',   // 500ms, 1s, 1.5s, 2s
})

Random Delay Range

// Add jitter to prevent thundering herd
const apiCall = withRetry(fetchData, {
  attempts: 3,
  delay: [1000, 3000],  // Random delay between 1-3 seconds
  backoff: 'exponential',
})

Conditional Retries

import { CruelRateLimitError, CruelHttpError } from 'cruel'

const smartRetry = withRetry(apiCall, {
  attempts: 5,
  delay: 1000,
  backoff: 'exponential',
  retryIf: (error) => {
    // Only retry rate limits and 5xx errors
    if (error instanceof CruelRateLimitError) return true
    if (error instanceof CruelHttpError) {
      return error.status >= 500
    }
    return false
  },
})

With Retry Callbacks

const monitoredFetch = withRetry(fetchData, {
  attempts: 3,
  delay: 1000,
  backoff: 'exponential',
  onRetry: (attempt, error) => {
    console.log(`Retry attempt ${attempt} after error: ${error.message}`)
    metrics.increment('api.retry', { attempt })
  },
})

Rate Limit Handling

import { CruelRateLimitError } from 'cruel'

const rateLimitAwareRetry = withRetry(apiCall, {
  attempts: 5,
  delay: 1000,
  backoff: 'exponential',
  maxDelay: 60000,
  retryIf: (error) => {
    return error instanceof CruelRateLimitError ||
           error.message.includes('rate limit')
  },
  onRetry: (attempt, error) => {
    if (error instanceof CruelRateLimitError && error.retryAfter) {
      console.log(`Rate limited, retry after ${error.retryAfter}s`)
    }
  },
})

Backoff Strategies

Fixed Backoff

Uses the same delay for all retries:
withRetry(fn, {
  attempts: 4,
  delay: 1000,
  backoff: 'fixed',
})
// Delays: 1s, 1s, 1s

Linear Backoff

Increases delay linearly with attempt number:
withRetry(fn, {
  attempts: 4,
  delay: 1000,
  backoff: 'linear',
})
// Delays: 1s, 2s, 3s

Exponential Backoff

Doubles the delay on each retry:
withRetry(fn, {
  attempts: 5,
  delay: 1000,
  backoff: 'exponential',
})
// Delays: 1s, 2s, 4s, 8s

Exponential with Jitter

Combine exponential backoff with random delay:
withRetry(fn, {
  attempts: 5,
  delay: [500, 1500],    // Random base delay
  backoff: 'exponential',
  maxDelay: 30000,
})
// Prevents thundering herd problem

Combining with Other Patterns

Retry + Timeout

import { withRetry, withTimeout } from 'cruel'

// Apply timeout first, then retry
const resilientFetch = withRetry(
  withTimeout(fetchData, { ms: 5000 }),
  {
    attempts: 3,
    delay: 1000,
    backoff: 'exponential',
  }
)

Retry + Circuit Breaker

import { withRetry, createCircuitBreaker } from 'cruel'

// Retry for transient failures, circuit breaker for sustained failures
const resilientAPI = createCircuitBreaker(
  withRetry(apiCall, {
    attempts: 3,
    delay: 1000,
    backoff: 'exponential',
  }),
  {
    threshold: 5,
    timeout: 30000,
  }
)

Retry + Fallback

import { withRetry, withFallback } from 'cruel'

// Try multiple times, then fall back
const safeAPI = withFallback(
  withRetry(apiCall, {
    attempts: 3,
    delay: 1000,
    backoff: 'exponential',
  }),
  {
    fallback: defaultValue,
  }
)

With Compose

import { cruel } from 'cruel'

const resilientAPI = cruel.compose(fetchData, {
  retry: {
    attempts: 5,
    delay: 1000,
    backoff: 'exponential',
    maxDelay: 30000,
  },
  timeoutMs: 10000,
  circuitBreaker: {
    threshold: 10,
    timeout: 60000,
  },
})

Advanced Examples

Custom Error Classification

const shouldRetry = (error: Error): boolean => {
  // Retry network errors
  if (error.name === 'NetworkError') return true
  
  // Retry 5xx server errors
  if (error instanceof CruelHttpError) {
    return error.status >= 500 && error.status < 600
  }
  
  // Retry timeouts
  if (error instanceof CruelTimeoutError) return true
  
  // Don't retry client errors (4xx)
  return false
}

const smartAPI = withRetry(apiCall, {
  attempts: 5,
  delay: 1000,
  backoff: 'exponential',
  retryIf: shouldRetry,
})

Database Retry with Deadlock Detection

const dbQuery = withRetry(executeQuery, {
  attempts: 5,
  delay: 100,
  backoff: 'exponential',
  maxDelay: 2000,
  retryIf: (error) => {
    // Retry deadlocks and lock timeouts
    return error.message.includes('deadlock') ||
           error.message.includes('lock timeout')
  },
  onRetry: (attempt, error) => {
    console.warn(`Database retry ${attempt}: ${error.message}`)
  },
})

Best Practices

  • Use exponential backoff for external APIs to give services time to recover
  • Add jitter (random delays) to prevent thundering herd problems
  • Set max delays to prevent excessive wait times
  • Be selective about what to retry using retryIf
  • Don’t retry client errors (4xx) - they won’t succeed on retry
  • Log retries for observability and debugging
  • Combine with timeouts to prevent hanging on slow operations
  • Consider circuit breakers for sustained failures

Common Retry Configurations

Use CaseAttemptsDelayBackoffMax Delay
External API3-51000msexponential30000ms
Rate limited API5-102000msexponential60000ms
Database query3100msexponential2000ms
File I/O3500mslinear2000ms
Critical operation101000msexponential60000ms

Build docs developers (and LLMs) love