Functions

Retry

Resilient retry strategies with fluent Schedule API

The Retry system provides resilient retry strategies for operations that might fail temporarily. It uses a fluent Schedule API inspired by Effect-TS, making it easy to compose retry strategies.

Why Use Retry?

Network operations, database connections, and external API calls often fail temporarily. Instead of failing immediately, retry the operation with a smart strategy:

  • Exponential backoff - Gradually increase delay between retries
  • Jitter - Add randomness to avoid thundering herd
  • Composable - Combine multiple strategies
  • Type-safe - Full TypeScript support

Basic Usage

Simple Retry

For 95% of cases, just pass a number:

import { retry } from '@deessejs/functions';

// Retry up to 3 times
const fetchWithRetry = retry(fetchUsers, 3);

// Use it
const users = await fetchWithRetry({ limit: 10 });

Using Presets

Common retry patterns are built-in:

import { retry, Schedule } from '@deessejs/functions';

// Network failures: exponential backoff + jitter + 5 attempts
const fetchWithRetry = retry(fetchUsers, Schedule.network());

// Quick retry: 3 immediate attempts
const quickRetry = retry(fetchData, Schedule.quick());

// Standard retry: 5 attempts with exponential backoff
const standardRetry = retry(fetchData, Schedule.standard());

// Safe retry: 5 spaced attempts (for idempotent operations)
const safeRetry = retry(fetchData, Schedule.safe());

Schedule API

Creating Schedules

Schedules are immutable values that can be composed:

import { Schedule } from '@deessejs/functions';

// Exponential backoff starting at 1000ms
const schedule1 = Schedule.exponential(1000);

// Add jitter (50% randomness by default)
const schedule2 = schedule1.jitter();

// Limit to 5 attempts
const schedule3 = schedule2.attempts(5);

// Combine in one line
const schedule = Schedule.exponential(1000).jitter().attempts(5);

Schedule Methods

Factory Methods

Create base schedules:

import { Schedule } from '@deessejs/functions';

// Immediate retry (no delay)
Schedule.immediate()

// Exponential backoff (base delay in ms)
Schedule.exponential(1000)  // 1000ms, 2000ms, 4000ms, 8000ms...

// Fixed delay between attempts
Schedule.spaced(2000)       // 2000ms, 2000ms, 2000ms...

// Fixed delay (alias for spaced)
Schedule.fixed(2000)

// Fibonacci sequence
Schedule.fibonacci(1000)    // 1000ms, 1000ms, 2000ms, 3000ms, 5000ms...

Modifiers

Transform existing schedules:

const schedule = Schedule.exponential(1000)
  .jitter(0.5)           // Add 50% randomness
  .attempts(5)           // Limit to 5 attempts
  .maxDelay(30000)       // Maximum 30s between retries
  .minDelay(1000);       // Minimum 1s between retries

Composition

Combine schedules:

// Use shorter delay
const schedule1 = Schedule.exponential(1000).or(Schedule.spaced(5000));

// Both constraints must be met
const schedule2 = Schedule.exponential(1000).and(Schedule.attempts(5));

// Sequence: 3 immediate, then spaced
const schedule3 = Schedule.attempts(3).immediate().then(Schedule.spaced(2000));

Conditions

Stop based on dynamic conditions:

// Stop when predicate returns true
const schedule1 = Schedule.forever().until((attempt, error) => {
  return attempt >= 5 || error.code === 'SUCCESS';
});

// Continue while predicate returns true
const schedule2 = Schedule.exponential(1000).while((attempt) => {
  return attempt < 10;
});

Side Effects

Add logging and monitoring:

const schedule = Schedule.exponential(1000)
  .attempts(5)
  .tap((attempt, delay) => {
    console.log(`Retry attempt ${attempt} after ${delay}ms`);
  })
  .log(); // Built-in logging

Examples

Network Request with Exponential Backoff

import { retry, Schedule } from '@deessejs/functions';

async function fetchUsers(): Promise<User[]> {
  const response = await fetch('https://api.example.com/users');
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

// Retry with exponential backoff + jitter, up to 5 attempts
const fetchWithRetry = retry(fetchUsers, Schedule.exponential(1000).jitter().attempts(5));

const users = await fetchWithRetry();

Database Connection with Preset

import { retry, Schedule } from '@deessejs/functions';

async function connectToDatabase(): Promise<Connection> {
  return await db.connect({
    host: 'localhost',
    port: 5432,
  });
}

// Use network preset (exponential + jitter + 5 attempts)
const connectWithRetry = retry(connectToDatabase, Schedule.network());

const connection = await connectWithRetry();

Fast Then Slow Strategy

import { Schedule } from '@deessejs/functions';

// 3 immediate retries, then switch to spaced retries
const schedule = Schedule.immediate().attempts(3).then(Schedule.spaced(2000));

const fetchWithStrategy = retry(fetchData, schedule);

Retry with Logging

import { retry, Schedule } from '@deessejs/functions';

const schedule = Schedule.exponential(1000)
  .attempts(5)
  .log(); // Built-in logging

const fetchWithLogging = retry(fetchUsers, schedule);

// Console output:
// [Schedule] Attempt 1 after 1000ms
// [Schedule] Attempt 2 after 2000ms
// [Schedule] Attempt 3 after 4000ms

Custom Logging

import { retry, Schedule } from '@deessejs/functions';

const schedule = Schedule.exponential(1000)
  .attempts(5)
  .tap((attempt, delay) => {
    metrics.record('retry_attempt', { attempt, delay });
    logger.info(`Retrying: attempt ${attempt}, delay ${delay}ms`);
  });

const fetchWithMetrics = retry(fetchUsers, schedule);

Stop on Specific Error

import { retry, Schedule } from '@deessejs/functions';

const schedule = Schedule.exponential(1000)
  .until((attempt, error) => {
    // Stop if error is not retryable
    if (error.code === 'AUTH_INVALID') {
      return true; // Stop retrying
    }
    return attempt >= 5;
  });

const fetchSmart = retry(fetchUsers, schedule);

Schedule Presets

Built-in presets for common scenarios:

import { Schedule } from '@deessejs/functions';

// Network retry (most common)
Schedule.network()
// Equivalent to:
Schedule.exponential(1000).jitter().attempts(5)

// Quick retry (for fast operations)
Schedule.quick()
// Equivalent to:
Schedule.attempts(3).immediate()

// Standard retry
Schedule.standard()
// Equivalent to:
Schedule.exponential(1000).attempts(5)

// Safe retry (for idempotent operations)
Schedule.safe()
// Equivalent to:
Schedule.spaced(1000).attempts(5)

Advanced Usage

Conditional Retry

import { retry, Schedule } from '@deessejs/functions';

// Only retry network errors
const schedule = Schedule.exponential(1000).attempts(5).until((attempt, error) => {
  // Don't retry authentication errors
  if (error.code === 'AUTH_INVALID') {
    return true; // Stop
  }
  // Retry network errors
  return false;
});

const fetchConditional = retry(fetchUsers, schedule);

Retry with Maximum Delay

import { Schedule } from '@deessejs/functions';

// Cap maximum delay at 30 seconds
const schedule = Schedule.exponential(1000)
  .maxDelay(30000)
  .attempts(10);

// Delays: 1000ms, 2000ms, 4000ms, 8000ms, 16000ms, 30000ms, 30000ms, ...

Compose Multiple Strategies

import { Schedule } from '@deessejs/functions';

// Use exponential, but capped at spaced intervals
const schedule = Schedule.exponential(1000)
  .or(Schedule.spaced(5000))  // Use shorter of exponential or 5s
  .attempts(5);

const fetchComposed = retry(fetchUsers, schedule);

Type Safety

Retry preserves function signatures:

import { retry, Schedule } from '@deessejs/functions';

async function fetchUser(id: number): Promise<User> {
  // ...
}

// ReturnType is preserved: (id: number) => Promise<User>
const fetchUserWithRetry = retry(fetchUser, Schedule.network());

// TypeScript knows types
const user: User = await fetchUserWithRetry(123);

Error Handling

Last Error is Thrown

If all retries fail, the last error is thrown:

import { retry, Schedule } from '@deessejs/functions';

try {
  await retry(fetchUsers, Schedule.attempts(3));
} catch (error) {
  console.error('All retries failed:', error.message);
}

Inspect Retry Attempts

import { retry, Schedule } from '@deessejs/functions';

let attemptCount = 0;

const schedule = Schedule.exponential(1000)
  .attempts(5)
  .tap((attempt) => {
    attemptCount = attempt;
  });

try {
  await retry(fetchUsers, schedule);
} catch (error) {
  console.log(`Failed after ${attemptCount} attempts`);
}

Best Practices

1. Use Presets for Common Cases

// ✅ Good - use preset
retry(fetchUsers, Schedule.network());

// ❌ Verbose - recreating preset
retry(fetchUsers, Schedule.exponential(1000).jitter().attempts(5));

2. Add Jitter for Distributed Systems

// ✅ Good - avoids thundering herd
retry(fetchUsers, Schedule.exponential(1000).jitter());

// ❌ Bad - synchronized retries in distributed systems
retry(fetchUsers, Schedule.exponential(1000));

3. Limit Maximum Attempts

// ✅ Good - bounded retries
retry(fetchUsers, Schedule.exponential(1000).attempts(5));

// ⚠️ Risky - could retry forever
retry(fetchUsers, Schedule.exponential(1000));

4. Use Spaced for Idempotent Operations

// ✅ Good - consistent delays for idempotent ops
retry(processPayment, Schedule.spaced(5000).attempts(3));

// ✅ Better - exponential for network requests
retry(fetchUsers, Schedule.exponential(1000).attempts(5));

Migration from Old API

The old config-based API is still supported but deprecated:

// ⚠️ Old - verbose config object (deprecated)
retry(fetchUsers, {
  maxAttempts: 5,
  initialDelay: 1000,
  backoffMultiplier: 2,
  maxDelay: 30000,
  jitter: true,
});

// ✅ New - fluent Schedule API (recommended)
retry(fetchUsers, Schedule.exponential(1000).jitter().attempts(5).maxDelay(30000));

See Also

  • AsyncResult - For handling async operations that can fail
  • Result - For type-safe error handling
  • Outcome - For distinguishing business failures from system errors

On this page