Andrew Grigorev d8af4e9f01
fix: skip retries for non-retryable errors in retryWithBackoff (#1082)
Add shouldRetry predicate to RetryOptions so callers can abort retries
for errors that will never succeed (e.g. 401 WorkflowValidationSkipError).

Previously, retryWithBackoff retried all errors blindly, wasting ~35s on
deterministic failures like workflow validation 401s.

Fixes #1081

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-04 20:14:47 -07:00

48 lines
1.3 KiB
TypeScript

export type RetryOptions = {
maxAttempts?: number;
initialDelayMs?: number;
maxDelayMs?: number;
backoffFactor?: number;
shouldRetry?: (error: Error) => boolean;
};
export async function retryWithBackoff<T>(
operation: () => Promise<T>,
options: RetryOptions = {},
): Promise<T> {
const {
maxAttempts = 3,
initialDelayMs = 5000,
maxDelayMs = 20000,
backoffFactor = 2,
shouldRetry,
} = options;
let delayMs = initialDelayMs;
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
console.log(`Attempt ${attempt} of ${maxAttempts}...`);
return await operation();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
console.error(`Attempt ${attempt} failed:`, lastError.message);
if (shouldRetry && !shouldRetry(lastError)) {
console.error("Error is not retryable, giving up immediately");
throw lastError;
}
if (attempt < maxAttempts) {
console.log(`Retrying in ${delayMs / 1000} seconds...`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
delayMs = Math.min(delayMs * backoffFactor, maxDelayMs);
}
}
}
console.error(`Operation failed after ${maxAttempts} attempts`);
throw lastError;
}