Skip to content

Instantly share code, notes, and snippets.

@pete-otaqui
Created December 30, 2024 09:25
Show Gist options
  • Select an option

  • Save pete-otaqui/ba912db2a18c1076c2a29a7a7fafd442 to your computer and use it in GitHub Desktop.

Select an option

Save pete-otaqui/ba912db2a18c1076c2a29a7a7fafd442 to your computer and use it in GitHub Desktop.

Revisions

  1. pete-otaqui created this gist Dec 30, 2024.
    78 changes: 78 additions & 0 deletions perform-with-backoff.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,78 @@
    /**
    * Perform a function with exponential backoff in the event of an error.
    *
    * @param fn The function to perform with backoff
    * @param options Options for the backoff
    * @returns The result of `fn()` after it has been successfully executed
    * @throws Any error thrown by `fn()` after the maximum number of attempts has
    * been reached, or if `options.retryError` returns false
    *
    * @example
    * ```ts
    * // Perform a synchronous function with backoff
    * const result1 = await performWithBackoff(() => {
    * return Math.random() > 0.5;
    * });
    * ```
    *
    * @example
    * ```ts
    * // Perform an asynchronous function with backoff
    * const result2 = await performWithBackoff(async () => {
    * await new Promise((resolve) => setTimeout(resolve, 1000));
    * return Math.random() > 0.5;
    * });
    * ```
    *
    * @example
    * ```ts
    * // Only accept certain errors for retrying
    * class RateLimitError extends Error {}
    * const result3 = await performWithBackoff(async () => {
    * // `fetch` might return a 429 status code if the API is rate limiting us
    * const response = await fetch("https://api.example.com");
    * if (response.status === 429) {
    * throw new RateLimitError('Rate limited');
    * }
    * return response;
    * }, {
    * retryError: (error) => error instanceof RateLimitError,
    * });
    *
    * ```
    */
    export async function performWithBackoff<T>(
    fn: () => Promise<T> | T,
    {
    maxAttempts = 5,
    calculateBackoffMs = (attempt: number) => {
    const jitter = Math.random() * attempt * 1000;
    const backoffMs = jitter + (attempt * attempt * 1000);
    return backoffMs;
    },
    retryError = (error: any) => true,
    logError = (error: any, attempt: number) => {
    console.log(`Attempt: ${attempt} failed with error: ${error}`);
    }
    }: {
    maxAttempts?: number;
    calculateBackoffMs?: (attempt: number) => number;
    retryError?: (error: any) => boolean;
    logError?: (error: any, attempt: number) => void | Promise<void>;
    } = {},
    ): Promise<T> {
    async function actuallyPerformWithBackoff(attempt: number): Promise<T> {
    try {
    return await fn();
    } catch (e) {
    if (attempt >= maxAttempts || !retryError(e)) {
    throw e;
    }
    await logError(e, attempt);
    const backoffMs = calculateBackoffMs(attempt);
    await new Promise((resolve) => setTimeout(resolve, backoffMs));
    return actuallyPerformWithBackoff(attempt + 1);
    }
    }
    return actuallyPerformWithBackoff(1);
    }