within-ts

Schedule

Retry and repeat with backoff strategies

Retry and repeat with backoff strategies. Resilient by default.

The problem

Network calls fail, APIs rate-limit, and databases experience transient errors. Writing correct retry logic by hand is error-prone:

async function fetchWithRetry(url: string) {
  for (let i = 0; i < 3; i++) {
    try {
      return await fetch(url)
    } catch (e) {
      if (i === 2) throw e
      await new Promise(r => setTimeout(r, Math.pow(2, i) * 100))
    }
  }
}

This approach does not compose. Every function that needs retries requires its own retry loop, and adding jitter, max duration, or logging means rewriting each one.

The solution

Schedule.retry works with Result — your function returns a Result, and retry checks !result.ok to decide whether to try again:

import { Schedule, Result } from "within-ts"

// callApi returns Result<Data, ApiError>
// Simple retry — retries on any Result.err
const result = await Schedule.retry(() => callApi(), {
  times: 3,
  delay: "100ms",
})

// Exponential backoff
const result = await Schedule.retry(() => callApi(), {
  times: 5,
  delay: Schedule.exponential("100ms"), // 100, 200, 400, 800, 1600
})

// Exponential with jitter and max delay
const result = await Schedule.retry(() => callApi(), {
  times: 5,
  delay: Schedule.exponential("100ms", { jitter: true, max: "5s" }),
})

// Retry only specific errors — while receives the unwrapped error
const result = await Schedule.retry(() => callApi(), {
  times: 3,
  delay: "1s",
  while: (error) => error instanceof RateLimitError,
})

The flow:

  1. Call the function
  2. If it returns Result.ok — done, return it
  3. If it returns Result.err — pass the error to while (if provided)
  4. If while returns true (or no while) — wait, retry
  5. If while returns false or retries exhausted — return the last Result.err

No exceptions are thrown at any point. The entire flow stays within the Result type.

On this page