Skip to content

Instantly share code, notes, and snippets.

@pete-otaqui
Created December 30, 2024 09:25
Show Gist options
  • Save pete-otaqui/ba912db2a18c1076c2a29a7a7fafd442 to your computer and use it in GitHub Desktop.
Save pete-otaqui/ba912db2a18c1076c2a29a7a7fafd442 to your computer and use it in GitHub Desktop.
perform with backoff
/**
* 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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment