PXD

useBackoffRetry

Runs an async task with exponential backoff and full jitter. Cancels in-flight work and pending waits when the effect scope is disposed, or when you call abort().

Native fetch does not reject on HTTP 4xx/5xx. Throw an error that carries a status (or use a custom retryable) if you want those responses to retry.

Exports

function useBackoffRetry(defaultConfig?: BackoffRetryConfig): UseBackoffRetryReturn
function isRetryableError(error: unknown): boolean

Types

interface BackoffRetryConfig {
  /** Maximum number of retries. @default 3 */
  maxRetries?: number
  /** Base delay in milliseconds. @default 500 */
  baseDelay?: number
  /** Maximum delay cap in milliseconds. @default 10000 */
  maxDelay?: number
  /** AbortSignal used to cancel in-flight work and backoff waits. */
  signal?: AbortSignal
  /**
   * Return `true` to retry, `false` to rethrow immediately.
   * Defaults to `isRetryableError`.
   */
  retryable?: (error: unknown) => boolean
  /** Called before each retry wait. */
  onRetry?: (attempt: number, delay: number, error: unknown) => void
}

interface UseBackoffRetryReturn {
  signal: AbortSignal
  abort: (reason?: any) => void
  retry: <T>(fn: (signal: AbortSignal) => Promise<T>, config?: BackoffRetryConfig) => Promise<T>
}

Params

NameTypeDescription
defaultConfig.maxRetriesnumberMaximum number of retries
defaultConfig.baseDelaynumberBase backoff delay in milliseconds
defaultConfig.maxDelaynumberMaximum backoff delay in milliseconds
defaultConfig.signalAbortSignalOptional external signal; otherwise a scope-owned controller is used
defaultConfig.retryable(error: unknown) => booleanPredicate for whether an error should be retried
defaultConfig.onRetry(attempt: number, delay: number, error: unknown) => voidHook invoked before each backoff wait

Returns

NameTypeDescription
retry(fn, config?) => Promise<T>Execute fn with backoff; fn receives the active AbortSignal
abort(reason?: any) => voidAbort the scope-owned controller
signalAbortSignalThe scope-owned abort signal

Per-call config overrides defaultConfig. Exhausted errors that are objects get an attempts property with the total try count.

Usage

import { useBackoffRetry } from 'pxd/composables'

const { retry, abort } = useBackoffRetry({
  maxRetries: 3,
  baseDelay: 500,
  onRetry: (attempt, delay, error) => {
    console.warn(`retry #${attempt}, wait ${delay}ms`, error)
  },
})

const data = await retry(async (signal) => {
  const res = await fetch('/api/data', { signal })
  if (!res.ok) {
    throw Object.assign(new Error(res.statusText), { status: res.status })
  }
  return res.json()
})

Custom retryable rule

const { retry } = useBackoffRetry({
  retryable: (error) => (error as { status?: number })?.status === 429,
})

isRetryableError treats 429 / 408 / 5xx, common network codes (ECONNRESET, ETIMEDOUT, …), Axios ERR_NETWORK / ERR_BAD_RESPONSE, and browser TypeError fetch failures as retryable.

Source

Source