You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通用化重试逻辑:指定重试/抛出异常类型并提升可读性

Generalizing Retry Logic with Configurable Exception Handling

Great question! To make your retry logic flexible, reusable, and easy to read, we can extract it into a dedicated helper method that lets you explicitly specify which exceptions trigger retries and which should be thrown immediately. Here's a step-by-step solution:

Step 1: Build a Reusable Retry Helper Class

We’ll create a static helper with both synchronous and asynchronous versions (since async is common in modern apps) that encapsulates all retry logic. The method will accept configurable parameters for retries, delays, and exception types.

Synchronous Version

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

public static class RetryHelper
{
    /// <summary>
    /// Executes an action with retry logic, allowing specification of fatal exceptions (no retry) and retryable exceptions.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    /// <param name="maxRetries">Total number of retry attempts allowed (total runs = maxRetries + 1).</param>
    /// <param name="delayBetweenRetries">Time to wait between retry attempts.</param>
    /// <param name="fatalExceptions">Exception types that should be rethrown immediately without retrying.</param>
    /// <param name="retryableExceptions">Exception types that trigger a retry attempt.</param>
    /// <exception cref="ArgumentNullException">Thrown if the action is null.</exception>
    /// <exception cref="ArgumentOutOfRangeException">Thrown if maxRetries is negative.</exception>
    /// <exception cref="AggregateException">Thrown if the action fails after all retries.</exception>
    public static void ExecuteWithRetry(
        Action action,
        int maxRetries,
        TimeSpan delayBetweenRetries,
        IEnumerable<Type> fatalExceptions,
        IEnumerable<Type> retryableExceptions)
    {
        // Validate input parameters first to catch errors early
        if (action == null)
            throw new ArgumentNullException(nameof(action));
        if (maxRetries < 0)
            throw new ArgumentOutOfRangeException(nameof(maxRetries), "Max retries cannot be negative.");

        var fatalExceptionTypes = fatalExceptions?.ToHashSet() ?? new HashSet<Type>();
        var retryableExceptionTypes = retryableExceptions?.ToHashSet() ?? new HashSet<Type>();

        int retryCount = 0;

        while (true)
        {
            try
            {
                action();
                return; // Success—exit the loop immediately
            }
            catch (Exception ex)
            {
                // Prioritize fatal exceptions (rethrow without retry)
                if (IsFatalException(ex, fatalExceptionTypes))
                {
                    throw;
                }

                // Check if this exception should trigger a retry
                if (!IsRetryableException(ex, retryableExceptionTypes))
                {
                    throw;
                }

                // If all retries are used up, wrap the exception for context
                if (retryCount >= maxRetries)
                {
                    throw new AggregateException(
                        $"Action failed after {maxRetries + 1} attempts.", ex);
                }

                // Wait before retrying
                Thread.Sleep(delayBetweenRetries);
                retryCount++;
            }
        }
    }

    /// <summary>
    /// Overload: Retry on all exceptions except the specified fatal ones.
    /// </summary>
    public static void ExecuteWithRetry(
        Action action,
        int maxRetries,
        TimeSpan delayBetweenRetries,
        params Type[] fatalExceptions)
    {
        ExecuteWithRetry(
            action,
            maxRetries,
            delayBetweenRetries,
            fatalExceptions,
            new[] { typeof(Exception) });
    }

    // Helper methods to clean up the main logic
    private static bool IsFatalException(Exception ex, HashSet<Type> fatalTypes)
    {
        return fatalTypes.Any(type => type.IsInstanceOfType(ex));
    }

    private static bool IsRetryableException(Exception ex, HashSet<Type> retryableTypes)
    {
        return retryableTypes.Any(type => type.IsInstanceOfType(ex));
    }
}

Asynchronous Version (Optional but Useful)

Add this to the RetryHelper class if you work with async/await workflows:

using System.Threading.Tasks;

public static async Task ExecuteWithRetryAsync(
    Func<Task> action,
    int maxRetries,
    TimeSpan delayBetweenRetries,
    IEnumerable<Type> fatalExceptions,
    IEnumerable<Type> retryableExceptions)
{
    if (action == null)
        throw new ArgumentNullException(nameof(action));
    if (maxRetries < 0)
        throw new ArgumentOutOfRangeException(nameof(maxRetries), "Max retries cannot be negative.");

    var fatalExceptionTypes = fatalExceptions?.ToHashSet() ?? new HashSet<Type>();
    var retryableExceptionTypes = retryableExceptions?.ToHashSet() ?? new HashSet<Type>();

    int retryCount = 0;

    while (true)
    {
        try
        {
            await action();
            return;
        }
        catch (Exception ex)
        {
            if (IsFatalException(ex, fatalExceptionTypes))
            {
                throw;
            }

            if (!IsRetryableException(ex, retryableExceptionTypes))
            {
                throw;
            }

            if (retryCount >= maxRetries)
            {
                throw new AggregateException(
                    $"Action failed after {maxRetries + 1} attempts.", ex);
            }

            await Task.Delay(delayBetweenRetries);
            retryCount++;
        }
    }
}

// Async overload for fatal exceptions only
public static async Task ExecuteWithRetryAsync(
    Func<Task> action,
    int maxRetries,
    TimeSpan delayBetweenRetries,
    params Type[] fatalExceptions)
{
    await ExecuteWithRetryAsync(
        action,
        maxRetries,
        delayBetweenRetries,
        fatalExceptions,
        new[] { typeof(Exception) });
}

Step 2: Simplify Your Original Code

Replace your messy retry loop with a clean, readable call to the helper. Using named parameters makes the intent crystal clear:

private const int RetrySegmentCount = 3;
private const int SecondsBetweenRetry = 30;

// Your original logic simplified
RetryHelper.ExecuteWithRetry(
    action: ExecuteProcessThatMayThrow,
    maxRetries: RetrySegmentCount,
    delayBetweenRetries: TimeSpan.FromSeconds(SecondsBetweenRetry),
    fatalExceptions: new[] { typeof(NotSupportedException) });

If you want to retry only on specific exceptions (e.g., IOException and TimeoutException), use the full overload:

RetryHelper.ExecuteWithRetry(
    action: ExecuteProcessThatMayThrow,
    maxRetries: RetrySegmentCount,
    delayBetweenRetries: TimeSpan.FromSeconds(SecondsBetweenRetry),
    fatalExceptions: new[] { typeof(NotSupportedException) },
    retryableExceptions: new[] { typeof(IOException), typeof(TimeoutException) });

Key Improvements

  • Flexibility: Configure exactly which exceptions to retry on and which to fail fast for.
  • Readability: Named parameters and XML comments make the code self-documenting.
  • Reusability: The helper can be used across your entire codebase instead of duplicating retry logic.
  • Robustness: Input validation catches errors early, and wrapping failures in AggregateException provides context about retry attempts.
  • Async Support: Seamlessly integrates with modern async/await workflows.

内容的提问来源于stack exchange,提问作者CuriousDeveloper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:33:32