Course contentsShow
← Rust
Lesson 863 of 1,9107. Error Handling PatternsFree lesson

Implementing Custom Retry Logic

Create reusable retry patterns for idempotent operations that fail transiently.

In this lesson, you will learn how to build a robust, reusable retry mechanism for idempotent operations that encounter transient failures.

The Strategy

When dealing with network requests or file system operations, some failures are "transient"—meaning the operation might succeed if you simply try again. However, you should only retry operations that are idempotent (performing the operation multiple times has the same effect as performing it once).

To implement this without cluttering your business logic, we use a higher-order function that accepts a closure.

Implementing the Retry Loop

The goal is to abstract the loop, the sleep duration, and the exit condition. Here is a simplified implementation:

use std::thread;
use std::time::Duration;

fn retry<T, E, F>(mut op: F, max_attempts: usize, delay: Duration) -> Result<T, E>
where
    F: FnMut() -> Result<T, E>,
{
    let mut attempts = 0;
    loop {
        match op() {
            Ok(val) => return Ok(val),
            Err(e) => {
                attempts += 1;
                if attempts >= max_attempts {
                    return Err(e);
                }
                thread::sleep(delay);
            }
        }
    }
}

Why this works

  1. Generic Constraints: By using F: FnMut() -> Result<T, E>, the retry function can wrap any fallible operation, keeping the logic decoupled from the specific error types.
  2. Control Flow: The loop gives us a clean way to exit early with Ok or exhaust our attempts and return the last Err.
  3. Idempotency: Because the caller provides the closure, they are responsible for ensuring that the operation is safe to repeat. Never use this pattern for non-idempotent actions (like withdrawing money from a bank account).

In production systems, you would likely extend this by adding "exponential backoff," where the delay increases after each failure to avoid overwhelming a struggling service. By wrapping this logic into a utility function, your main code remains readable, focusing on the "what" rather than the repetitive "how" of error recovery.

Key Takeaway: Encapsulate retry logic in a reusable higher-order function to keep your business code clean while safely handling transient failures for idempotent operations.