smart_retry 1.0.0 copy "smart_retry: ^1.0.0" to clipboard
smart_retry: ^1.0.0 copied to clipboard

A production-ready retry library for Dart and Flutter featuring exponential backoff, advanced jitter strategies, conditional retries, typed exceptions, event streams, and zero-boilerplate async resilience.

smart_retry πŸ”„ #

Stop writing the same try/catch/for-loop boilerplate in every project.

pub.flutter-io.cn Dart SDK License: MIT style: lints

A production-ready, pure-Dart package for intelligent async retry logic:

  • βœ… Exponential / linear / constant backoff β€” grows delay between retries
  • βœ… Full / equal / decorrelated jitter β€” prevents thundering herd problems
  • βœ… retryIf predicate β€” only retry transient errors, abort on permanent ones
  • βœ… onRetry callback β€” rich [RetryContext] before each sleep
  • βœ… Lifecycle event stream β€” plug in any logger / crash reporter
  • βœ… previewSchedule β€” inspect the delay table without executing
  • βœ… Typed exceptions β€” MaxAttemptsExceededException & NonRetryableException
  • βœ… Zero runtime dependencies (only meta)
  • βœ… 100% Pure Dart β€” Flutter, Dart CLI, server-side

Table of Contents #

  1. Installation
  2. Quick Start
  3. API Reference
  4. Exception Handling
  5. Delay Schedule Reference
  6. Strategy Comparison
  7. Advanced Recipes
  8. Contributing
  9. License

Installation #

# pubspec.yaml
dependencies:
  smart_retry: ^1.0.0
dart pub get
# or
flutter pub get

Quick Start #

import 'dart:io';
import 'package:smart_retry/smart_retry.dart';

// ── Minimal ──────────────────────────────────────────────────────────────────
final data = await SmartRetry.run(
  () => api.fetchUser(id: 42),
  maxAttempts: 5,
  retryIf: (e) => e is SocketException,
);

// ── With logging ──────────────────────────────────────────────────────────────
final data = await SmartRetry.run(
  () => api.fetchUser(id: 42),
  maxAttempts: 5,
  retryIf: (e) => e is SocketException || e is TimeoutException,
  onRetry: (ctx) => print(
    '[${ctx.attemptNumber}/${ctx.maxAttempts}] '
    'Retry in ${ctx.nextDelay.inMilliseconds}ms β€” ${ctx.lastException}',
  ),
);

// ── Handle errors explicitly ─────────────────────────────────────────────────
try {
  final data = await SmartRetry.run(() => api.fetchUser(id: 42));
} on MaxAttemptsExceededException catch (e) {
  print('Failed after ${e.attempts} tries: ${e.lastException}');
} on NonRetryableException catch (e) {
  print('Permanent error, not retrying: ${e.cause}');
}

API Reference #

SmartRetry.run #

The primary API. All parameters are optional with production-ready defaults.

static Future<T> run<T>(
  Future<T> Function() fn, {
  int maxAttempts = 3,
  Duration initialDelay = const Duration(milliseconds: 500),
  Duration maxDelay = const Duration(seconds: 30),
  double factor = 2.0,
  BackoffStrategy backoffStrategy = BackoffStrategy.exponential,
  JitterStrategy jitterStrategy = JitterStrategy.full,
  FutureOr<bool> Function(Exception e)? retryIf,
  void Function(RetryContext context)? onRetry,
  bool debugMode = false,
})
Parameter Type Default Description
fn Future<T> Function() β€” The async callable to execute and retry.
maxAttempts int 3 Total tries including the first.
initialDelay Duration 500ms Base delay before the 2nd attempt.
maxDelay Duration 30s Hard cap on any single delay.
factor double 2.0 Exponential growth multiplier.
backoffStrategy BackoffStrategy exponential Delay growth curve.
jitterStrategy JitterStrategy full Randomisation mode.
retryIf FutureOr<bool> Function(Exception)? null Per-exception retry gate. Returns true β†’ retry, false β†’ abort.
onRetry void Function(RetryContext)? null Callback invoked before each retry sleep.
debugMode bool false Print structured debug output.

SmartRetry.runWithOptions #

Preferred for shared configuration across multiple call-sites.

static Future<T> runWithOptions<T>(
  Future<T> Function() fn, {
  required RetryOptions options,
})
// Define once
final _retry = RetryOptions(
  maxAttempts: 4,
  retryIf: (e) => e is SocketException,
);

// Reuse everywhere
final user  = await SmartRetry.runWithOptions(() => api.getUser(),  options: _retry);
final posts = await SmartRetry.runWithOptions(() => api.getPosts(), options: _retry);

RetryOptions #

Immutable configuration value object. Supports copyWith for derivation.

const RetryOptions({
  int maxAttempts = 3,
  Duration initialDelay = const Duration(milliseconds: 500),
  Duration maxDelay = const Duration(seconds: 30),
  double factor = 2.0,
  BackoffStrategy backoffStrategy = BackoffStrategy.exponential,
  JitterStrategy jitterStrategy = JitterStrategy.full,
  FutureOr<bool> Function(Exception e)? retryIf,
  void Function(RetryContext context)? onRetry,
  bool debugMode = false,
})
// Base config
const base = RetryOptions(maxAttempts: 4, retryIf: _isTransient);

// Derived config β€” inherits base, overrides specific fields
final upload = base.copyWith(maxAttempts: 8, maxDelay: Duration(minutes: 2));

BackoffStrategy #

Controls how the base delay grows between attempts.

Strategy Formula Example (initial=500ms, factor=2)
exponential min(cap, initial Γ— factorⁿ) 500 β†’ 1000 β†’ 2000 β†’ 4000ms
linear min(cap, initial Γ— (n+1)) 500 β†’ 1000 β†’ 1500 β†’ 2000ms
constant initial (always) 500 β†’ 500 β†’ 500 β†’ 500ms

JitterStrategy #

Controls randomisation applied on top of the base delay.

Strategy Formula Notes
full random(0, base) Best total throughput under load
equal base/2 + random(0, base/2) Guarantees minimum half delay
decorrelated random(initial, min(cap, lastΓ—3)) Best under high contention
none base (no randomisation) Tests / single-client jobs only

RetryContext #

Passed to onRetry before every retry sleep.

Field Type Description
attemptNumber int 1-based number of the attempt that just failed.
maxAttempts int Total configured attempts.
lastException Exception Exception thrown by the last attempt.
nextDelay Duration Actual sleep duration (post-jitter).
elapsedTime Duration Wall-clock time since the first attempt.
remainingAttempts int Computed: maxAttempts - attemptNumber.
isFinalAttempt bool True if the next attempt is the last.

Event Stream #

SmartRetry.events is a broadcast stream of [RetryEvent] instances.

SmartRetry.events.listen((event) {
  switch (event) {
    case AttemptStarted(:final attemptNumber, :final maxAttempts):
      print('β–Ά $attemptNumber/$maxAttempts');

    case AttemptFailed(:final attemptNumber, :final exception, :final nextDelay):
      logger.warn('Attempt $attemptNumber failed (${nextDelay.inMilliseconds}ms): $exception');

    case AttemptSucceeded(:final attemptNumber, :final totalElapsed):
      metrics.increment('retry.success', tags: {'attempt': '$attemptNumber'});

    case AllAttemptsFailed(:final totalAttempts, :final lastException):
      Sentry.captureException(lastException);

    case RetryAborted(:final exception):
      logger.error('Non-retryable: $exception');
  }
});

Available event types: AttemptStarted, AttemptFailed, AttemptSucceeded, AllAttemptsFailed, RetryAborted.


previewSchedule #

Inspect the full delay table without executing any code.

final schedule = SmartRetry.previewSchedule(
  options: RetryOptions(
    maxAttempts: 5,
    initialDelay: Duration(milliseconds: 500),
    factor: 2.0,
    backoffStrategy: BackoffStrategy.exponential,
    jitterStrategy: JitterStrategy.none,
  ),
);

for (final row in schedule) {
  print('Retry #${row.attempt}: ${row.actualDelay.inMilliseconds}ms');
}
// Retry #1: 500ms
// Retry #2: 1000ms
// Retry #3: 2000ms
// Retry #4: 4000ms

Exception Handling #

try {
  final result = await SmartRetry.run(() => api.getData());
} on MaxAttemptsExceededException catch (e) {
  // All attempts failed β€” transient problem persisted too long
  print('Gave up after ${e.attempts} attempts in ${e.totalElapsed.inSeconds}s');
  print('Last error: ${e.lastException}');
} on NonRetryableException catch (e) {
  // retryIf returned false β€” permanent error, no point retrying
  print('Permanent failure: ${e.cause}');
  print('Reason: ${e.message}');
}

Delay Schedule Reference #

Default configuration (maxAttempts: 5, initialDelay: 500ms, factor: 2.0, jitter: full):

Retry Base Delay After Full Jitter
1st 500ms 0 – 500ms
2nd 1 000ms 0 – 1 000ms
3rd 2 000ms 0 – 2 000ms
4th 4 000ms 0 – 4 000ms

Strategy Comparison #

BackoffStrategy JitterStrategy Best for
exponential full Default β€” general network calls
exponential equal When a minimum wait is required
exponential decorrelated High-concurrency, many clients
constant none Tests, deterministic queue consumers
linear full APIs with linear rate-limit windows

Advanced Recipes #

Async retryIf predicate #

await SmartRetry.run(
  () => api.upload(file),
  retryIf: (e) async {
    // Check connectivity before deciding to retry
    final connected = await connectivity.checkConnectivity();
    return connected && e is SocketException;
  },
);

Shared config across a service class #

class ApiService {
  static final _opts = RetryOptions(
    maxAttempts: 4,
    initialDelay: const Duration(milliseconds: 300),
    retryIf: (e) => e is SocketException || e is TimeoutException,
    onRetry: (ctx) => log.warn('[API] retry ${ctx.attemptNumber}'),
  );

  Future<User> getUser(int id) =>
      SmartRetry.runWithOptions(() => _http.get('/users/$id'), options: _opts);

  Future<List<Post>> getPosts() =>
      SmartRetry.runWithOptions(() => _http.get('/posts'), options: _opts);
}

Plug in Sentry / Firebase Crashlytics #

void setupRetryMonitoring() {
  SmartRetry.events.listen((event) {
    if (event is AllAttemptsFailed) {
      Sentry.captureException(event.lastException, hint: Hint.withMap({
        'attempts': '${event.totalAttempts}',
      }));
    }
  });
}

Contributing #

Pull requests and issues are welcome at github.com/Brah-Timo/smart_retry.

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/my-feature
  3. Run tests: dart test
  4. Run the linter: dart analyze
  5. Submit a PR

License #

MIT Β© 2026

0
likes
150
points
6
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A production-ready retry library for Dart and Flutter featuring exponential backoff, advanced jitter strategies, conditional retries, typed exceptions, event streams, and zero-boilerplate async resilience.

Repository (GitHub)
View/report issues

Topics

#retry #backoff #network #resilience #http

License

MIT (license)

Dependencies

meta

More

Packages that depend on smart_retry