Skip to content

Instantly share code, notes, and snippets.

@vitaly-t
Last active April 13, 2025 09:21
Show Gist options
  • Save vitaly-t/6e3d285854d882b1618c7e435df164c4 to your computer and use it in GitHub Desktop.
Save vitaly-t/6e3d285854d882b1618c7e435df164c4 to your computer and use it in GitHub Desktop.
retry-async
/**
* Retry-status object type, for use with RetryCB.
*/
export type RetryStatus<D = unknown> = {
/**
* Retry index, starting from 0.
*/
readonly index: number,
/**
* Retry overall duration, in milliseconds.
*/
readonly duration: number,
/**
* Last error, if available;
* it is undefined only when "retryAsync" calls "func" with index = 0.
*/
readonly error?: Error,
/**
* Extra data for status handlers, if specified.
*/
readonly data?: D
};
/**
* Retry-status callback type.
*/
export type RetryCB<T, D = unknown> = (s: RetryStatus<D>) => T;
/**
* Type for options passed into retryAsync function.
*/
export type RetryOptions<D = unknown> = {
/**
* Maximum number of retries (infinite by default),
* or a callback to indicate the need for another retry.
*/
readonly retry?: number | RetryCB<boolean, D>,
/**
* Retry delays, in milliseconds (no delay by default),
* or a callback that returns the delays.
*/
readonly delay?: number | RetryCB<number, D>,
/**
* Error notifications.
*/
readonly error?: RetryCB<void, D>,
/**
* Extra data for status handlers.
*/
readonly data?: D
};
/**
* Retries async operation returned from "func" callback, according to "options".
*/
export function retryAsync<T, D>(func: RetryCB<Promise<T>, D>, options?: RetryOptions<D>): Promise<T> {
const start = Date.now();
let index = 0, e: any;
let {retry = Number.POSITIVE_INFINITY, delay = -1, error, data} = options ?? {};
const s = () => ({index, duration: Date.now() - start, error: e, data});
const c = (): Promise<T> => func(s()).catch(err => {
e = err;
typeof error === 'function' && error(s());
if ((typeof retry === 'function' ? (retry(s()) ? 1 : 0) : retry--) <= 0) {
return Promise.reject(e);
}
const d = typeof delay === 'function' ? delay(s()) : delay;
index++;
return d >= 0 ? (new Promise(a => setTimeout(a, d))).then(c) : c();
});
return c();
}
@vitaly-t
Copy link
Author

vitaly-t commented Apr 7, 2025

The most recent update added templated option {data: D}, which can be used as an extra parameter inside status handlers.

For example, you can use it for implementing your own external retry-strategy, like shown below:

import {retryAsync, RetryCB, RetryStatus} from './retry-async';

type RetryStrategy = 'http' | 'generic';

function retryForStrategy(s: RetryStatus<RetryStrategy>): boolean {
    // 3 attempts for generic operations, and 5 for http:
    return s.data === 'http' ? s.index < 5 : s.index < 3;
}

function delayForStrategy(s: RetryStatus<RetryStrategy>): number {
    // 500ms for generic operations, incremental 500ms for http:
    return s.data === 'http' ? (s.index + 1) * 500 : 500;
}

function retryAsyncStrategy<T>(func: RetryCB<Promise<T>, RetryStrategy>, strategy?: RetryStrategy): Promise<T> {
    return retryAsync(func, {retry: retryForStrategy, delay: delayForStrategy, data: strategy});
}

So above, if you pass optional "http" parameter into retryAsyncStrategy, it will execute a different retry strategy. It's just a more elegant way to abstract retry strategies away from the code that relies on them, through simple parametrization.

UPDATE:

For the example above, it would make sense to replace the verbose retryAsyncStrategy with two simpler functions, as below:

function retryHttp<T>(func: RetryCB<Promise<T>, RetryStrategy>): Promise<T> {
    return retryAsync(func, {retry: retryForStrategy, delay: delayForStrategy, data: 'http'});
}

function retryGeneric<T>(func: RetryCB<Promise<T>>): Promise<T> {
    return retryAsync(func, {retry: retryForStrategy, delay: delayForStrategy});
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment