Skip to content

Retry Utility

Two retry helpers on RetryHelper. One retries when a call throws. The other retries when a call succeeds but the result is not what you want yet.

In one example

typescript
import { RetryHelper } from '@venizia/ignis-helpers';

// Retries because the call THREW
const data = await RetryHelper.executeWithRetry({
  operation: 'fetch-remote-config',
  execution: () => fetchConfig(),
  maxAttempts: 5,
});

// Retries because the result is not YET what we want
const order = await RetryHelper.executeWithRetryUntil({
  operation: 'wait-for-paid-order',
  execution: () => orderRepository.findById({ id: orderId }),
  until: result => result?.status === 'PAID',
  maxAttempts: 5,
});

Works in the browser

Import from @venizia/ignis-helpers/core instead, and every method above is available - the whole class is part of the browser-pure surface, verified by the repo's purity gate rather than by inspection.

typescript
import { RetryHelper } from '@venizia/ignis-helpers/core';

Which one do I need?

MethodRetries whenTypical use
RetryHelper.executeWithRetryexecution throwsFlaky network calls, connection setup
RetryHelper.executeWithRetryUntiluntil(result) returns falsePolling until data is fresh or a job is done. Powers the repository retry option.

Both share the same backoff engine and the same habit: on exhaustion, log one logger.warn and hand back the LAST outcome.

RetryHelper.executeWithRetry

typescript
static executeWithRetry: <T>(opts: {
  operation: string;
  execution: (context: { attempt: number; signal?: AbortSignal }) => ValueOrPromise<T>;
  maxAttempts?: number; // default 3
  maxTotalMs?: number; // total budget across attempts and sleeps
  perAttemptTimeoutMs?: number; // race each attempt against a timeout
  backoff?: IRetryBackoffOptions;
  shouldRetry?: (context: IRetryContext) => boolean;
  onRetry?: (context: IRetryContext & { nextDelayMs: number }) => ValueOrPromise<void>;
  signal?: AbortSignal;
  logger?: ILogger;
}) => Promise<T>;

The rules:

  • Every thrown error retries, unless shouldRetry returns false - then it rethrows immediately. Use this for permanent errors like a 400.
  • Out of attempts or budget? The LAST error is thrown.
  • signal aborts between attempts and during sleeps. It is also passed to execution - a running promise cannot be cancelled from outside, so honor it inside if you can.

RetryHelper.executeWithRetryUntil

typescript
static executeWithRetryUntil: <T>(opts: {
  operation: string;
  execution: (context: { attempt: number; signal?: AbortSignal }) => ValueOrPromise<T>;
  until: (result: T) => boolean; // return true to stop: "the result is good"
  maxAttempts?: number; // default 3
  maxTotalMs?: number; // stop starting NEW attempts after this much time
  backoff?: IRetryBackoffOptions;
  signal?: AbortSignal;
  logger?: ILogger;
}) => Promise<T>;

The rules:

  • A thrown error is never retried. It rethrows immediately. Only a successful call with a "not yet" result retries.
  • Out of attempts or budget? The LAST result is returned as-is. No error.
  • maxTotalMs never cuts a running read short. It only stops NEW attempts from starting. Zero or negative just means "no retries" - one call still runs.
  • maxAttempts below 1 throws before anything runs.
  • An aborted signal rejects the call - a cancelled caller does not want a stale result.

Use it for any polling: waiting for a job status to flip, for a downstream service to come up, for a replica to catch up.

Backoff and jitter

Both helpers wait between attempts using IRetryBackoffOptions:

typescript
interface IRetryBackoffOptions {
  strategy?: 'fixed' | 'linear' | 'exponential' | 'schedule'; // default exponential
  initialDelayMs?: number; // default 250
  multiplier?: number; // exponential growth factor, default 2
  maxDelayMs?: number; // cap before jitter, default 30000
  scheduleMs?: readonly number[]; // required for 'schedule'
  jitter?: 'none' | 'full' | 'equal'; // default full
}
StrategyDelay for attempt N
fixedinitialDelayMs
linearinitialDelayMs * N
exponentialinitialDelayMs * multiplier ** (N - 1)
schedulescheduleMs[N - 1], last entry repeats
JitterEffect
nonedelay used as-is
fullrandom in [0, delay)
equalrandom in [delay/2, delay)

Prefer named constants? RetryBackoffStrategies.EXPONENTIAL, RetryJitterModes.EQUAL, etc.

NOTE

These defaults (250ms, 30s cap) suit network retries. The repository retry option uses its own tighter defaults (50ms up to 500ms) - see Read Retry.

Other exports

ExportWhat it does
RetryHelper.runWithTimeout({ operation, timeoutMs, execution })Races execution against a timeout. Omitted or <= 0 means no timeout.
RetryHelper.isRetryTimeoutError(error)true when the error is a timeout from runWithTimeout/executeWithRetry.
RetryHelper.computeBackoffDelayMs({ attempt, backoff })The delay both helpers use, exposed for your own loops.

See also

Files: