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...