This post is a follow on from Implementing a simple retry pattern in c#.

Tasks, async and await are rapidly becoming be default API flavours in many dotnet libraries and the performance benefits for IO bound code have been well documented. However if you need to apply the retry pattern to some async or task returning method invocation you need to watch out for subtle errors. I'll outline the problem and revise my solution from the previous post.

The problem

Examine the following code and spot the subtle bug:

var maxRetryAttempts = 3;  
var pauseBetweenFailures = TimeSpan.FromSeconds(2);  
RetryHelper.RetryOnException  
    (maxRetryAttempts, pauseBetweenFailures, () => {
   // Don't do this
    httpClient.DeleteAsync(
        "https://example.com/api/products/1");
}); 

Find it? The issue is that the HTTP delete request is async and returns a Task that we do not capture, so the lambda will continue execution and complete before the HTTP request completes. If the HTTP request fails, say there was a temporary server error, the retry will never occur because the exception is on another thread and the lambda has already completed.

Whenever you have an async method or a method that returns a task you must capture the returned task and await it. The best advice is to do "async all the way down" and avoid any calls to Task.Wait() or Tasks.WaitAll(). More info on async await is available here.

So we must refactor our previous RetryHelper implementation to await the return of the lambda and pass its task result back to the caller so that exceptions can bubble up in the correct way.

A solution

I have made a few improvements to the previous version to support async code:

  • Return a Task so that the caller can await the RetryHelper to complete or fail.
  • Await the async lambda so the result or exception is passed back properly.
  • Added a generics overload to allow callers to only retry a particular type of exception. (This could also be applied to the synchronous version).
  • Added an Async suffix to the method name which is the Microsoft convention best practice.

And the sample usage would be as so:

var maxRetryAttempts = 3;  
var pauseBetweenFailures = TimeSpan.FromSeconds(2);  
await RetryHelper.RetryOnExceptionAsync<HttpRequestException>  
    (maxRetryAttempts, pauseBetweenFailures, async () => {
    var response = await httpClient.DeleteAsync(
        "https://example.com/api/products/1");
    response.EnsureSuccessStatusCode();
}); 

The lambda function now carries the async await syntax and we are awaiting the entire retry helper. Also notice that we are now only retrying exceptions that are sensible to retry, and we are using EnsureSuccessStatusCode() on HttpClient to throw the exception if the HTTP request failed.

So now there is no excuse for intermittent network or server errors to cause your apps to fail, and your code does not need confusing retry loops scattered throughout it. Feel free to copy/paste it into your next project.