General Retry Strategy #3 with TryResult I wanted to use the Try helper from @DmitryNogin's General Retry Strategy #2 but each attempt to implement it revealed another thing that is missing.

In my review I mentioned that it would be useful to be able to log the exceptions but later I came to the conclusion that perhaps it would be better to implement something similar to the ParallelLoopResult - this is how the TryResult was formed.

public struct TryResult
{
    public TryResult(bool isCompleted, IEnumerable&lt;Exception&gt; exceptions) : this()
    {
        IsCompleted = isCompleted;
        Exceptions = exceptions;
    }

    public bool IsCompleted { get; private set; }

    public IEnumerable&lt;Exception&gt; Exceptions { get; private set; }
}


Then I added it to the base class:

public abstract class Try
{
    public static readonly Try Never = new Never();
    public static readonly Try Once = Retry(delay: 0, times: 0, ratio: 0);

    public static Try Retry(int delay, int times, double ratio) =&gt;
        RetryAfter(from i in Enumerable.Range(0, times)
                   select delay * Math.Pow(ratio, i) into d
                   select (int)d);

    public static Try RetryAfter(params int[] delays) =&gt; RetryAfter(delays.AsEnumerable());
    public static Try RetryAfter(IEnumerable&lt;int&gt; delays) =&gt; new Retry(delays);

    public void Execute(Action action) =&gt; Execute(action, CancellationToken.None);
    public abstract TryResult Execute(Action action, CancellationToken cancellationToken);

    public Task ExecuteAsync(Action action) =&gt; ExecuteAsync(action, CancellationToken.None);
    public Task ExecuteAsync(Action action, CancellationToken cancellationToken) =&gt;
        ExecuteAsync(() =&gt; { action(); return Task.CompletedTask; }, cancellationToken);

    public Task ExecuteAsync(Func&lt;Task&gt; action) =&gt; ExecuteAsync(action, CancellationToken.None);
    public abstract Task&lt;TryResult&gt; ExecuteAsync(Func&lt;Task&gt; action, CancellationToken cancellationToken);

}


and adjusted the implementations:

class Never : Try
{
    public override TryResult Execute(Action action, CancellationToken cancellationToken)
    {
        return new TryResult(true, Enumerable.Empty&lt;Exception&gt;());
    }
    public override Task&lt;TryResult&gt; ExecuteAsync(Func&lt;Task&gt; action, CancellationToken cancellationToken) 
        =&gt; Task.FromResult&lt;TryResult&gt;(new TryResult(true, Enumerable.Empty&lt;Exception&gt;()));
}


the same for Retry:

class Retry : Try
{
    IEnumerable&lt;int&gt; Delays { get; }

    public Retry(IEnumerable&lt;int&gt; delays)
    {
        Delays = delays;
    }

    public override TryResult Execute(Action action, CancellationToken cancellationToken)
    {
        var exceptions = new List&lt;Exception&gt;();

        foreach (var delay in Delays)
        {
            try
            {
                action();
                return new TryResult(true, exceptions);
            }
            catch
            {
                cancellationToken.WaitHandle.WaitOne(delay);
                cancellationToken.ThrowIfCancellationRequested();
            }
        }
        return new TryResult(false, exceptions);
    }

    public override async Task&lt;TryResult&gt; ExecuteAsync(Func&lt;Task&gt; action, CancellationToken cancellationToken)
    {
        var exceptions = new List&lt;Exception&gt;();

        foreach (var delay in Delays)
        {
            try
            {
                await action();
                return new TryResult(true, exceptions);
            }
            catch
            {
                await Task.Delay(delay, cancellationToken);
            }
        }

        return new TryResult(false, exceptions);
    }
}
Let’s put the cart before the horse – I mean Catch before the Try :)

class Program
{
    static void Main(string[] args)
    {
        using (new Catch&lt;InvalidOperationException&gt;())
        using (new Catch&lt;FormatException&gt;(ex =&gt; Console.WriteLine("Oops!")))
            Try.Retry(100, 3, 3).Execute(() =&gt;
            {
                Console.WriteLine("Trying!");
                throw new FormatException();
            });
    }
}


These using Catch statements define exceptions we can tolerate and continue trying. All other exceptions will be interpreted as critical. We can also rethrow or log in the optional handler.

Library classes:

public class Catch&lt;TException&gt; : Catch
    where TException : Exception
{
    public Catch()
        : this(ex =&gt; { })
    {
    }

    public Catch(Action&lt;TException&gt; handler)
    {
        Handler = handler;
    }

    protected internal override bool HandleCore(Exception ex)
    {
        if (ex is TException)
        {
            Handler(ex as TException);
            return true;
        }

        if (Previous == null)
            throw ex;
        else
            return Previous.HandleCore(ex);
    }

    Action&lt;TException&gt; Handler { get; }
}


And:

public abstract class Catch : Ambient&lt;Catch&gt;
{
    public static void Handle(Exception ex) =&gt;
        Current?.HandleCore(ex);

    protected internal abstract bool HandleCore(Exception ex);
}


And:

public abstract class Ambient&lt;T&gt; : IDisposable where T : Ambient&lt;T&gt;
{
    static readonly string Id = typeof(T).FullName;
    protected static T Current
    {
        get { return (T)CallContext.LogicalGetData(Id); }
        set { CallContext.LogicalSetData(Id, value); }
    }

    protected Ambient()
    {
        Previous = Current;
        Current = (T)this;
    }

    public void Dispose() =&gt; Current = Previous;
    protected T Previous { get; }
}


And we also need to update Retry strategy:

    public override void Execute(Action action, CancellationToken cancellationToken)
    {
        foreach (var delay in Delays)
            try
            {
                action();
                return;
            }
            catch(Exception ex)
            {
                Catch.Handle(ex); // &lt;- NEW LINE
                cancellationToken.WaitHandle.WaitOne(delay);
                cancellationToken.ThrowIfCancellationRequested();
            }

        action();
    }




UPDATE

Let's have this for the Log class - note Log.IfFail():

class Log : Ambient&lt;Log&gt;
{
    public static Exception IfFail(Action action)
    {
        try
        {
            action();
            return null;
        }
        catch(Exception ex)
        {
            Write(ex.Message);
            return ex;
        }
    }

    public static void Write(string line) =&gt;
        Current?.WriteCore(line);

    public Log(string fileName)
    {
        FileName = fileName;
    }

    void WriteCore(string line) =&gt; File.AppendAllText(FileName, line);
    string FileName { get; }
}


And:

public static class Error
{
    public static void Rethrow(this Exception ex)
    {
        if (ex != null)
            throw ex;
    }
}


So we can have TryExecute:

    public override bool TryExecute(Action action, CancellationToken cancellationToken)
    {
        foreach (var delay in Delays)
            try
            {
                Log.IfFail(action).Rethrow();
                return true;
            }
            catch(Exception ex)
            {
                Catch.Handle(ex);
                cancellationToken.WaitHandle.WaitOne(delay);
                cancellationToken.ThrowIfCancellationRequested();
            }

        return Log.IfFail(action) == null; // yes, ugly :) let's invent something better
    }


We can pass more context for logging at the moment of ambient log construction.



UPDATE #2 :)

public static class Error
{
    public static void Rethrow(this Exception ex)
    {
        if (ex != null)
            throw ex;
    }

    public static bool ToTryResult(this Exception ex)
    {
        return ex == null;
    }
}


And:

    public override bool TryExecute(Action action, CancellationToken cancellationToken)
    {
        foreach (var delay in Delays)
            try
            {
                Log.IfFail(action).Rethrow();
                return true;
            }
            catch(Exception ex)
            {
                Catch.Handle(ex);
                cancellationToken.WaitHandle.WaitOne(delay);
                cancellationToken.ThrowIfCancellationRequested();
            }

        return Log.IfFail(action).ToTryResult();
    }