Skip to content

Instantly share code, notes, and snippets.

@iiison
Created June 1, 2025 11:20
Show Gist options
  • Save iiison/5e915c82315e8133a838f4d534838f4e to your computer and use it in GitHub Desktop.
Save iiison/5e915c82315e8133a838f4d534838f4e to your computer and use it in GitHub Desktop.
A RequestRetry class in JS
type PromiseFunc = (...args: any[]) => Promise<any>;
type Params<Func extends PromiseFunc> = {
func: Func;
delay: number;
maxTries: number;
shouldJitter: boolean;
signal?: AbortSignal | undefined;
shouldRetry: (e: Error) => boolean;
};
export class RequestRetry<Func extends PromiseFunc> {
private func: Func;
private delay: Params<Func>["delay"];
private maxTries: Params<Func>["maxTries"];
private shouldRetry: Params<Func>["shouldRetry"];
private shouldJitter: boolean;
private signal?: AbortSignal | undefined;
private tryCount = 0;
constructor({
func,
signal,
delay = 1000,
maxTries = 5,
shouldJitter = true,
shouldRetry = () => true,
}: Params<Func>) {
this.func = func;
this.delay = delay;
this.maxTries = maxTries;
this.shouldJitter = shouldJitter;
this.shouldRetry = shouldRetry;
this.signal = signal;
}
private getDelayWithJitter(): number {
const exponentialDelay = this.delay * 1.5;
if (!this.shouldJitter) return exponentialDelay;
// Apply jitter: ±30%
const jitterPercentage = 0.3;
const jitter =
(Math.random() * 2 - 1) * jitterPercentage * exponentialDelay;
// range: [-30%, +30%]
const finalDelay = exponentialDelay + jitter;
return finalDelay;
}
private sleep(time: number) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, time);
if (this.signal) {
this.signal.addEventListener(
"abort",
() => {
clearTimeout(timeout);
reject(new Error("❌ Aborted"));
},
{ once: true }
);
}
});
}
private async retry(
e: Error,
args: Parameters<Func>
): Promise<Awaited<ReturnType<Func>>> {
if (this.signal?.aborted) throw new Error("❌ Aborted");
const shouldTry = await this.shouldRetry(e);
if (!shouldTry || this.maxTries < this.tryCount) {
throw e;
}
try {
this.tryCount += 1;
await this.sleep(this.delay);
const result = await this.func(...args);
return result;
} catch (error: unknown) {
this.delay = Math.round(this.getDelayWithJitter());
return await this.retry(error as Error, args);
}
}
async run(...args: Parameters<Func>): Promise<Awaited<ReturnType<Func>>> {
try {
const res = await this.func(...args);
return res;
} catch (e: unknown) {
return await this.retry(e as Error, args);
}
}
}
function flakyFetch() {
return new Promise((resolve, reject) => {
const success = Math.random() > 0.7; // 30% success rate
setTimeout(() => {
if (success) {
resolve("✅ Success on attempt!");
} else {
reject("❌ Random failure");
}
}, 200);
});
}
const controller = new AbortController()
const retry = new RequestRetry({
func: flakyFetch,
delay: 500,
maxTries: 15,
signal: controller.signal,
shouldRetry: (e) => {
console.log("Retry condition evaluated for:", e);
return true;
}
,
});
retry
.run("this is sparta!")
.then( (result) => console.log("Final result:", result))
.catch( (error) => console.error("Final failure:", error));
setTimeout( () => { controller.abort() }, 1000)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment