retry<T> static method

Future<Result<T>> retry<T>(
  1. Future<Result<T>> operation(), {
  2. int maxAttempts = 3,
  3. Duration delay = const Duration(milliseconds: 500),
  4. double backoffFactor = 2.0,
  5. Duration? maxDelay,
  6. double jitter = 0.0,
  7. Random? random,
  8. Duration? attemptTimeout,
  9. bool retryIf(
    1. Failure failure
    )?,
  10. void onRetry(
    1. int attempt,
    2. Failure failure
    )?,
})

Re-runs operation up to maxAttempts times.

Returns the first Success produced. If every attempt fails, returns the final Error. If retryIf returns false for a failure, retries stop immediately and that failure is returned.

onRetry is invoked after a failure that will be retried, with the 1-based attempt number that failed and the failure itself. It is not invoked for the final failure or for a Success.

maxDelay caps the wait between attempts after exponential backoff is applied. Use this to bound the worst-case wait when maxAttempts and backoffFactor are both large.

jitter adds randomness in the range [0, jitter] as a multiplier on the computed wait — e.g. jitter: 0.3 produces waits between 100% and 130% of the backed-off delay. Defaults to 0.0 (no jitter). Useful for preventing many clients from retrying in lockstep after a shared outage. Pass random to make the jitter deterministic in tests.

attemptTimeout bounds each individual attempt. If operation does not produce a Result within this duration, the attempt is converted into an Error(Failure.timeout()) and is subject to the normal retryIf / maxAttempts logic. Leave null to wait indefinitely on each attempt.

Implementation

static Future<Result<T>> retry<T>(
  Future<Result<T>> Function() operation, {
  int maxAttempts = 3,
  Duration delay = const Duration(milliseconds: 500),
  double backoffFactor = 2.0,
  Duration? maxDelay,
  double jitter = 0.0,
  Random? random,
  Duration? attemptTimeout,
  bool Function(Failure failure)? retryIf,
  void Function(int attempt, Failure failure)? onRetry,
}) async {
  assert(maxAttempts > 0, 'maxAttempts must be > 0');
  assert(backoffFactor > 0, 'backoffFactor must be > 0');
  assert(jitter >= 0, 'jitter must be >= 0');

  final rng = random ?? Random();

  Result<T>? last;
  for (var attempt = 1; attempt <= maxAttempts; attempt++) {
    Result<T> result;
    try {
      result = attemptTimeout != null
          ? await operation().timeout(attemptTimeout)
          : await operation();
    } on TimeoutException catch (e, st) {
      result = Error<T>(
        Failure.timeout(
          message: 'Attempt timed out after $attemptTimeout',
          stackTrace: st,
          cause: e,
        ),
      );
    }
    last = result;

    if (result is Success<T>) return result;

    final failure = (result as Error<T>).failure;
    final isLastAttempt = attempt == maxAttempts;
    final shouldRetry = retryIf?.call(failure) ?? true;

    if (isLastAttempt || !shouldRetry) return result;

    onRetry?.call(attempt, failure);

    var waitMicros =
        (delay.inMicroseconds * _pow(backoffFactor, attempt - 1)).round();
    if (maxDelay != null && waitMicros > maxDelay.inMicroseconds) {
      waitMicros = maxDelay.inMicroseconds;
    }
    if (jitter > 0) {
      waitMicros = (waitMicros * (1 + rng.nextDouble() * jitter)).round();
    }
    await Future<void>.delayed(Duration(microseconds: waitMicros));
  }
  // Unreachable: the loop above always returns once it exits.
  return last!;
}