Search
⌘K

Retry

Utilities for retrying code execution.

Example Usage

retry

Return a new function that internally calls the original function, with retry logic on top.

The function to wrap must be async, and must never throw.

import { retry } from './retry';

// NOTE: this function must be async, and never throw
async function myOperation(a:number, b:number) {
  const data = (a+b) * Math.random();
  const isSuccess = data > 0.5;
  if (!isSuccess) return { ok: false }
  return { ok: true }
} 

// Usage 1: run inline

// NOTE: don't forget the extra `()` at the end
const result = await retry({
  fn: myOperation,
  times: 10,
  delayBetweenAttempts: 100,
  getStatus: (result) => {
    if (result.ok) return 'success';
    return 'error';
  }
})(10,50);


// Usage 2: create then run

const myOperationWithRetry = retry({
  fn: myOperation,
  times: 10,
  delayBetweenAttempts: 100,
  getStatus: (result) => {
    if (result.ok) return 'success';
    return 'error';
  }
});
await myOperationWithRetry(10,50);

Dependencies

No dependencies

Auto Install

npx shadcn@latest add https://shadcn-registry-ts.vercel.app/r/util-retry.json

Manual Install

retry.ts
import { sleep } from "./sleep";


/**
 * Wrap an async function and add retry feature.  
 * IMPORTANT: `fn`  must return a Promise, and must never throw.
 */
export const retry = <
  TFn extends ((...args: any[]) => Promise<any>),
  TFnResult = Awaited<ReturnType<TFn>>
>(options: {
  /** 
   * The main function, that will retried.  
   * **IMPORTANT** Must be an async fn, and must never throw. 
   * */
  fn: TFn,
  /** How many times to retry while the `getStatus` returns 'error'. */
  times: number,
  /** How long to sleep between attempts. In ms. */
  delayBetweenAttempts: number,
  /** This function is called after each attempt to know if the attempt was successful or not.  
   * This fn must return `success` or `error`.  
   * If `success` is returned, the loop will exit.  
   * If `error` is returned, the loop will continue if `times` is not reached. 
   * */
  getStatus: (result: TFnResult) => 'success' | 'error';
}) => {

  return async (...args: Parameters<TFn>): Promise<TFnResult> => {
    let attemptsDone = 0;

    while (attemptsDone < options.times) {

      // increment
      attemptsDone += 1;

      // run fn
      const result = (await options.fn(...args)) as TFnResult;
      const status = options.getStatus(result);

      // if success -> return
      if (status === 'success') {
        return result;
      }

      // if error and last attempt -> exit with error
      if (status === 'error' && (attemptsDone >= options.times)) {
        return result;
      }

      // if error and not last attempt -> sleep then try again
      await sleep(options.delayBetweenAttempts);
    }

    throw new Error('Should be unreachable');

  };

};

Test

retry.test.ts
import { describe, it, expect } from 'vitest';
import { repeatAsyncFnInParallel } from '../utility-framework/vitest.utils';

import { retry } from './retry';

// utils

const fnThatAlwaysFails = async () => ({ ok: false } as const);
const fnThatAlwaysSucceeds = async () => ({ ok: true } as const);
const fnThatRandomlyFails = async () => Math.random() > 0.5 ? fnThatAlwaysSucceeds() : fnThatAlwaysFails();
const fnThatRandomlyFailsWithArguments = async (a: number, b: number) => Math.random() > 0.5 ? ({ ok: true, data: a + b } as const) : fnThatAlwaysFails();
// tests

describe('retry', () => {

  it('do it - always success', async () => {

    const ITERATIONS = 10_000;
    const FREQUENCIES = {
      success: 0,
      error: 0,
    };

    await repeatAsyncFnInParallel(ITERATIONS, async () => {
      const result = await retry({
        fn: fnThatAlwaysSucceeds,
        times: 10,
        delayBetweenAttempts: 100,
        getStatus: (result) => result.ok ? 'success' : 'error'
      })();

      expect(result).toMatchObject({ ok: true });

      if (result.ok) FREQUENCIES.success++;
      else FREQUENCIES.error++;
    });

    expect(FREQUENCIES.success).toBe(ITERATIONS);
    expect(FREQUENCIES.error).toBe(0);
  });

  it('do it - always fails', async () => {

    const ITERATIONS = 10_000;
    const FREQUENCIES = {
      success: 0,
      error: 0,
    };

    await repeatAsyncFnInParallel(ITERATIONS, async () => {
      const result = await retry({
        fn: fnThatAlwaysFails,
        times: 10,
        delayBetweenAttempts: 100,
        getStatus: (result) => result.ok ? 'success' : 'error'
      })();

      expect(result).toMatchObject({ ok: false });

      if (result.ok) FREQUENCIES.success++;
      else FREQUENCIES.error++;
    });

    expect(FREQUENCIES.success).toBe(0);
    expect(FREQUENCIES.error).toBe(ITERATIONS);
  });

  it('do it - randomly fails', async () => {

    const ITERATIONS = 10_000;
    const FREQUENCIES = {
      success: 0,
      error: 0,
    };

    await repeatAsyncFnInParallel(ITERATIONS, async () => {
      const result = await retry({
        fn: fnThatRandomlyFails,
        times: 10,
        delayBetweenAttempts: 100,
        getStatus: (result) => result.ok ? 'success' : 'error'
      })();

      expect(result).toHaveProperty('ok');
      expect(result.ok).toBeOneOf([true, false]);

      if (result.ok) FREQUENCIES.success++;
      else FREQUENCIES.error++;
    });

    expect(FREQUENCIES.success).toBeGreaterThan(0);
    expect(FREQUENCIES.error).toBeGreaterThan(0);
  });

  it('do it - with arguments', async () => {

    const ITERATIONS = 10_000;
    const FREQUENCIES = {
      success: 0,
      error: 0,
    };

    await repeatAsyncFnInParallel(ITERATIONS, async () => {
      const result = await retry({
        fn: fnThatRandomlyFailsWithArguments,
        times: 10,
        delayBetweenAttempts: 100,
        getStatus: (result) => result.ok ? 'success' : 'error'
      })(3, 4);

      if (result.ok === false) {
        expect(result).toMatchObject({ ok: false });
        FREQUENCIES.error++;
        return;
      }
      else {
        expect(result).toMatchObject({ ok: true, data: 7 });
        FREQUENCIES.success++;
      }
    });

    expect(FREQUENCIES.success).toBeGreaterThan(0);
    expect(FREQUENCIES.error).toBeGreaterThan(0);

  });

});

Command Palette

Search for a command to run...