khalid13
khalid13

Reputation: 2847

Can I catch an Exception of a type passed in by generic parameter in c#?

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

Answers (2)

Eric Lippert
Eric Lippert

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

nick
nick

Reputation: 2853

I think your issue is caused by the fact that your func is async. I would try something like this for async funcs

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

Related Questions