Reputation: 2847
Take this code for example - I think that when a TException is shown, I should be able to "catch" it and retry my func()
an appropriate number of times. But when I put this code in the wild, even though an exception of type TException is thrown, it skips the catch clause and gets bubbled up. Can someone explain why?
public static T TryNTimes<T, TException>(Func<T> func, int times) where TException : Exception
{
if (times <= 0)
throw new ArgumentException($"TryNTimes: `times` must be a positive integer. You passed in: {times}");
while (times > 0)
{
try
{
return func();
}
catch (TException)
{
if (--times <= 0)
throw;
}
}
// should never reach here
return default(T);
}
Code is being called like this:
await RetryUtils.TryNTimes<Task, MyCustomException>(
() => TryHandleElasticMappingError(dataGridResults, dataGridRecords),
MyFieldsCount)
.ConfigureAwait(false);
Any chance it's an async-ness issue? The above line is wrapped in a Try-Catch which catches an Exception
where I'm able to verify the type of the Exception is MyCustomException
. I can confirm that the inner catch block (the one in the retry method) is never hit.
Upvotes: 1
Views: 276
Reputation: 660533
Any chance it's an async-ness issue?
As noted in the other answer, yes, it is an async issue.
Async and iterator blocks in C# are coroutines. A normal routine can do three things: run to completion, throw, or go into an infinite loop. A coroutine can do a fourth thing: suspend, to be resumed later. An await
is a point in an async block where a suspension happens; it's yield return
in an iterator block.
In your case the throw doesn't happen until the coroutine resumes, at which point the try
is no longer in effect; the method with the try
ran to completion because it is not a coroutine. If you want the try
to be in effect then the try has to be in an async block also, and you have to await
inside the try.
Similarly if you wrote:
IEnumerable<int> Weird(bool b)
{
if (b) throw new Exception();
yield return 1;
}
...
IEnumerable<int> x = null;
try
{
x = Weird(true); // Should throw, right?
}
catch(Exception ex)
{
// Nope; this is unreachable
}
foreach(int y in x) // the throw happens here!
An iterator block coroutine begins suspended, and does not resume until MoveNext
is called on the iterator; all it does is return an iterator. An async block coroutine suspends on an await, but is not required to suspend on an await; an await of an already-completed task is permitted to skip the suspension.
try-catch with coroutines is tricky; be careful!
Upvotes: 6
Reputation: 2853
I think your issue is caused by the fact that your func
is async. I would try something like this for async func
s
using System;
using System.Threading.Tasks;
namespace GenericException
{
public static class FuncHelpers
{
public static async Task<T> TryNTimesAsync<T,TException>(this Func<Task<T>> func, int n) where TException : Exception
{
while(n --> 0)
{
try
{
return await func();
}
catch(TException)
{
if(n < 0)
throw;
}
}
return default(T);
}
public static async Task TryNTimesAsync<TException>(this Func<Task> func, int n) where TException : Exception
{
while(n --> 0)
{
try
{
await func();
}
catch(TException)
{
if(n <= 0) throw;
}
}
}
public static T TryNTimes<T,TException>(this Func<T> func, int n) where TException : Exception
{
while(n --> 0)
{
try
{
return func();
}
catch(TException)
{
if(n <= 0) throw;
}
}
return default(T);
}
}
class Program
{
static Task AddTwoNumbersAsync(int num1, int num2)
{
var task = new Task(() =>
{
if(num1 % 2 == 0) throw new ArgumentException($"{nameof(num1)} must be odd");
Console.WriteLine($"{num1} + {num2} = {num1 + num2}");
});
task.Start();
return task;
}
static async Task Main(string[] args)
{
Func<Task> addTask = () => AddTwoNumbersAsync(2, 4);
Console.WriteLine("trying to add non async");
var taskResult = addTask.TryNTimes<Task, ArgumentException>(5);
Console.WriteLine("trying to add async");
try
{
await addTask.TryNTimesAsync<ArgumentException>(5);
}
catch(ArgumentException)
{
Console.WriteLine("There was an error trying to add async");
}
}
}
}
As to why it's not working as expected, I agree with @Hans Kilian.
Basically, your func
is returning a Task
, or a promise to do something. That thing hasn't necessarily completed running by the time that Task
is completed. The reason you would catch your exception outside of this method is when you're calling .ConfigureAwait(false)
, you're waiting for that Task
to complete, so it would throw its exception.
Edit: Full sample code, including async runner for just Task
vs Task<T>
Upvotes: 0