Claude-skill-registry asyncredux-retry-mixin

Add the Retry mixin for automatic retry with exponential backoff on action failure. Covers using Retry alone for limited retries, combining with UnlimitedRetries for infinite retries, and configuring retry behavior.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/asyncredux-retry-mixin" ~/.claude/skills/majiayu000-claude-skill-registry-asyncredux-retry-mixin && rm -rf "$T"
manifest: skills/data/asyncredux-retry-mixin/SKILL.md
source content

Retry Mixin

The

Retry
mixin automatically retries failed actions using exponential backoff. When an error occurs in the
reduce()
method, the action is re-executed with progressively increasing delays between attempts.

Basic Usage

Add the

Retry
mixin to any action that should automatically retry on failure:

class LoadDataAction extends AppAction with Retry {
  Future<AppState?> reduce() async {
    var data = await fetchDataFromServer();
    return state.copy(data: data);
  }
}

With this mixin:

  • If the action fails, it automatically retries up to 3 times (default)
  • Each retry waits longer than the previous (exponential backoff)
  • If all retries fail, the original error is thrown

Configuration Parameters

Override these getters to customize retry behavior:

class LoadDataAction extends AppAction with Retry {

  // Delay before first retry (default: 350ms)
  int get initialDelay => 500;

  // Multiplier for delay growth (default: 2)
  int get multiplier => 2;

  // Maximum retry attempts (default: 3)
  int get maxRetries => 5;

  // Upper limit on delay to prevent excessive waits (default: 5000ms)
  int get maxDelay => 10000;

  Future<AppState?> reduce() async {
    var data = await fetchDataFromServer();
    return state.copy(data: data);
  }
}
ParameterDefaultPurpose
initialDelay
350 msWaiting period before first retry
multiplier
2Growth factor for delays between attempts
maxRetries
3Maximum retry count (total executions = maxRetries + 1)
maxDelay
5 secUpper limit on delay to prevent excessive waits

Retry Sequence Example

With default settings (initialDelay=350ms, multiplier=2, maxRetries=3):

  1. Initial attempt - Action runs, fails
  2. Wait 350ms - First retry, fails
  3. Wait 700ms - Second retry, fails
  4. Wait 1400ms - Third retry, fails
  5. Error thrown - All retries exhausted

Timing Considerations

Retry delays start after the reducer finishes, not from when the action was dispatched. If

reduce()
takes 1 second to fail and
initialDelay
is 350ms, the first retry starts 1.35 seconds after the action began.

Tracking Retry Attempts

Access the

attempts
getter within your action to know which attempt is currently running:

class LoadDataAction extends AppAction with Retry {
  Future<AppState?> reduce() async {
    print('Attempt ${attempts + 1}'); // 0-indexed, so first attempt is 0

    if (attempts > 0) {
      // Maybe try a different server on retries
      return state.copy(data: await fetchFromBackupServer());
    }

    return state.copy(data: await fetchFromPrimaryServer());
  }
}

Unlimited Retries

Combine

UnlimitedRetries
with
Retry
to retry indefinitely until the action succeeds:

class CriticalSyncAction extends AppAction with Retry, UnlimitedRetries {
  Future<AppState?> reduce() async {
    await syncCriticalData();
    return state.copy(syncComplete: true);
  }
}

This is equivalent to setting

maxRetries
to
-1
.

Warning: Using

await dispatchAndWait(action)
with
UnlimitedRetries
may hang indefinitely if the action continues failing. Use with caution and consider whether the action has a realistic chance of eventually succeeding.

Important Behavior Notes

Only reduce() Failures Trigger Retry

The

Retry
mixin only retries when errors occur in the
reduce()
method. Failures in the
before()
method do not trigger retries - they fail immediately.

class LoadDataAction extends AppAction with Retry {

  @override
  Future<void> before() async {
    // Errors here will NOT trigger retry - action fails immediately
    await validatePermissions();
  }

  Future<AppState?> reduce() async {
    // Only errors here trigger the retry mechanism
    return state.copy(data: await fetchData());
  }
}

Actions Become Asynchronous

All actions using the

Retry
mixin become asynchronous, regardless of their original synchronous nature. This is because the retry mechanism needs to wait between attempts.

Combining with NonReentrant (Best Practice)

Most actions using

Retry
should also include the
NonReentrant
mixin to prevent multiple instances from running simultaneously:

class SaveDataAction extends AppAction with NonReentrant, Retry {
  Future<AppState?> reduce() async {
    await saveToServer();
    return state.copy(saved: true);
  }
}

This prevents scenarios where:

  • User clicks "Save" multiple times
  • Multiple retry sequences run in parallel
  • Server receives duplicate or conflicting requests

Combining with CheckInternet

For network operations, combine

Retry
with
CheckInternet
to ensure connectivity before attempting the action:

class FetchUserProfile extends AppAction with CheckInternet, Retry {
  Future<AppState?> reduce() async {
    var profile = await api.getUserProfile();
    return state.copy(profile: profile);
  }
}

The

CheckInternet
mixin runs first. If there's no connection, the action fails immediately without attempting retries.

Common Use Cases

API Calls with Transient Failures

class FetchProductsAction extends AppAction with Retry {
  int get maxRetries => 3;
  int get initialDelay => 500;

  Future<AppState?> reduce() async {
    var products = await api.getProducts();
    return state.copy(products: products);
  }
}

Critical Sync Operations

class SyncPendingChanges extends AppAction with Retry, UnlimitedRetries {
  int get initialDelay => 1000;
  int get maxDelay => 30000; // Cap at 30 seconds between retries

  Future<AppState?> reduce() async {
    await syncService.pushPendingChanges();
    return state.copy(hasPendingChanges: false);
  }
}

Payment Processing with Extended Retries

class ProcessPaymentAction extends AppAction with NonReentrant, Retry {
  final double amount;

  ProcessPaymentAction(this.amount);

  int get maxRetries => 5;
  int get initialDelay => 1000;
  int get multiplier => 2;
  int get maxDelay => 10000;

  Future<AppState?> reduce() async {
    var result = await paymentGateway.process(amount);
    return state.copy(paymentStatus: result.status);
  }
}

Mixin Compatibility

Compatible with:

  • CheckInternet
  • NoDialog
  • AbortWhenNoInternet
  • NonReentrant
  • Throttle
  • Debounce

Can be combined with:

  • UnlimitedRetries
    (enables infinite retries)

Full Example with All Options

class RobustApiAction extends AppAction
    with CheckInternet, NonReentrant, Retry {

  // Retry configuration
  int get initialDelay => 500;     // 500ms before first retry
  int get multiplier => 2;          // Double delay each time
  int get maxRetries => 4;          // Try up to 5 times total
  int get maxDelay => 8000;         // Never wait more than 8 seconds

  Future<AppState?> reduce() async {
    if (attempts > 0) {
      print('Retry attempt $attempts');
    }

    var data = await api.fetchCriticalData();
    return state.copy(data: data);
  }
}

References

URLs from the documentation: