2

When awaiting a Task I would like to have an easy way to ignore specific types of exceptions, like OperationCanceledException or TimeoutException or whatever. I got the idea of writing an extension method that could wrap my Task, and suppress the Exception type I would give it as argument. So I wrote this one:

public static async Task Ignore<TException>(this Task task)
    where TException : Exception
{
    try
    {
        await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return;
        throw;
    }
}

I can use it like this and it works OK:

await myTask.Ignore<OperationCanceledException>();

The problem is that it supports only one type of exception, and I must write another version for two types, another one for three types, another one for four, and so on. This is getting out of hand, because I also want an overload of this extension that will ignore exceptions of tasks that return a result (Task<Result>). So I will need another series of extension methods to cover this case as well.

Here is my implementation for ignoring one type of exception when awaiting a Task<Result>:

public static async Task<TResult> Ignore<TResult, TException>(
    this Task<TResult> task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

Usage:

var result = await myInt32Task.Ignore<int, OperationCanceledException>(0);

My question is, can I write these methods in a way that can handle multiple types of ignored exceptions, without having to write a separate method for each number of types?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104

4 Answers4

1

Yes you can, but you cannot do it using Generics.

If you're willing to pass Types as params, you can do this:

public static async Task<TResult> Ignore<TResult>
        (this Task<TResult> task, TResult defaultValue, params Type[] typesToIgnore)
        {
            try
            {
                return await task;
            }
            catch (Exception ex)
            {
                if (typesToIgnore.Any(type => type.IsAssignableFrom(ex.GetType())))
                {
                    return defaultValue;
                }

                throw;
            }
        }

Now, this is much less attractive and you don't have the generic constraint (where TException...) but it should get the job done.

Aage
  • 5,932
  • 2
  • 32
  • 57
  • 1
    This is a good idea Aage! I noticed though a difference in behavior. My generic version ignores derived types of exceptions too. For example ignoring the `OperationCanceledException` ignores the derived `TaskCanceledException` as well. Your version ignores only the specific type of exception passed as argument, which is too strict IMHO. Is it possible to make it behave like the original? – Theodor Zoulias Oct 19 '19 at 18:10
  • @TheodorZoulias [Of course](https://stackoverflow.com/a/2742288/10102452). You would probably need something like `typesToIgnore.Any(t => t.IsSubclassOf(ex.GetType()) || t == ex.GetType());` Or turn the check into an extension method like "IsSameOrSubclass", as suggested in the linked answer, and use that one instead! – Vector Sigma Oct 20 '19 at 01:26
  • @VectorSigma Yeap, I will probably combine the two answers to get the best of both. :-) – Theodor Zoulias Oct 20 '19 at 01:42
  • @TheodorZoulias I didn't realise your solution had the extra behaviour of ignoring subtypes, I updated my code to do that as well. – Aage Oct 20 '19 at 06:46
  • Now it is excellent! My problem now is that I have too many good answers to choose from. :-) – Theodor Zoulias Oct 20 '19 at 09:14
1

I would rely on task chaining to avoid initializing task execution in the extension method.

public static Task<TResult> Ignore<TResult>(this Task<TResult> self, TResult defaultValue, params Type[] typesToIgnore)
{
    return self.ContinueWith(
        task =>
        {
            if (task.IsCanceled 
                && (typesToIgnore.Any(t => typeof(OperationCanceledException) == t || t.IsSubclassOf(typeof(OperationCanceledException))))) {
                return defaultValue;
            }

            if (!task.IsFaulted)
            {
                return task.Result;
            }

            if (typesToIgnore.Any(t => task.Exception.InnerException.GetType() == t ||
                                task.Exception.InnerException.GetType().IsSubclassOf(t)))
            {
                return defaultValue;
            }

            throw task.Exception.InnerException;
        }, TaskContinuationOptions.ExecuteSynchronously);
}
vladimir
  • 13,428
  • 2
  • 44
  • 70
  • Thanks Vladimir for the answer. For some reason this method causes a `TaskCanceledException` when the main task completes successfully! – Theodor Zoulias Oct 19 '19 at 19:22
  • Fixed - the continuation with option *TaskContinuationOptions.OnlyOnFaulted* couldn't be executed on the completed antecedent task that cause this exception. – vladimir Oct 19 '19 at 23:59
  • Now it works! There is still a problem though. The exceptions `OperationCanceledException` and `TaskCanceledException` cannot be ignored for some reason. This is my async method: `async Task GetAsync() => throw new OperationCanceledException();` I am awaiting it like this: `await GetAsync().Ignore(0, typeof(OperationCanceledException));` and it throws. – Theodor Zoulias Oct 20 '19 at 00:33
  • Good point, these exceptions are not interpreted as Faulted-status. Fixed. Now the 'task cancel' exceptions cannot be distinguished, in other words, when defining *typesToIgnore* as *TaskCanceledException* will be ignored not only *TaskCanceledException* but *OperationCanceledException* too. – vladimir Oct 20 '19 at 01:26
  • Thanks Vladimir, now it works like a charm! About `TaskCanceledException`, I never catch/check for this type anyway. In my code I prefer to check for the more general `OperationCanceledException`. – Theodor Zoulias Oct 20 '19 at 01:40
1

From what I understand, your desire is to be able to ignore more than 1 type of exceptions while awaiting your Task. Your own solution appears to be your best option for me. You could always simply "chain" the calls using your proposed solution:

await myTask.Ignore<OperationCanceledException>().Ignore<IOException>().Ignore<TimeoutException>();

This should return a Task that is, in essence, three nested try-catch blocks. Unless you actively want a more elegant definition, you can always get away with more elegant usage ;)

The only not-so-elegant problem is that, in the case of your TResult-returning Tasks, you have to "propagate" the default value multiple times. If this is not a very big problem, then you could get away with the same:

await myTask.Ignore<int, OperationCanceledException>(0).Ignore<int, TimeoutException>(0);

As an "obscure" bonus, note that this way you can very easily supply different default return values for different exceptions. So having to repeat the default value might turn to your advantage after all! For example you might have a reason to return 0 on TimeOutException but -1 on OperationCanceledException, etc. If this becomes your purpose in the end, remember that using is might not be what you really want, rather than exact Type equality, because you might want to also return different default values for different exceptions that derive from the same Type (this analysis is starting to get rather complicated but you get the point, of course).

Update

The ultimate level of chained-call "elegance" for the TResult-based version seems to have to come at the expense of compile-time type checking:

public static async Task<TResult> Ignore<TResult, TException>(
this Task<TResult> task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

public static async Task<TResult> Ignore<TResult, TException>(
this Task task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await (Task<TResult>)task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

public static Task Ignore<TException>(this Task task)
    where TException : Exception
{
    try
    {
        //await seems to create a new Task that is NOT the original task variable.
        //Therefore, trying to cast it later will fail because this is not a Task<TResult>
        //anymore (the Task<TResult> has been "swallowed").

        //For that reason, await the task in an independent function.
        Func<Task> awaitableCallback = async () => await task;

        awaitableCallback();

        //And return the original Task, so that it can be cast back correctly if necessary.
        return task;
    }
    catch (Exception ex)
    {
        //Same upon failure, return the original task.
        if (ex is TException) return task;
        throw;
    }
}

public static async Task<int> TestUse()
{
    Task<int> t = Task<int>.Run(() => 111);

    int result = await t.Ignore<TaskCanceledException>()
                        .Ignore<InvalidOperationException>()
                        .Ignore<int, TimeoutException>(0);

    return result;
}

If you are prepared to sacrifice compile-time safety, you can ease the pain of repetition by only stating the exceptions you wish to ignore and adding the "casting" call in the end. This has its own share of problems, of course, but you only need to do it when you need to ignore multiple exceptions. Otherwise, you are good with a single type and the corresponding single call to Ignore<TResult, TException>().

Edit

Based on the relevant comment, because the async/await pattern appears to spawn a new Task that wraps the awaitable task parameter passed in the Ignore methods above, the example call indeed fails with an InvalidCastException as the intermediate Ignore calls in fact changed the Task and the original Task gets lost somewhere in the chain of calls. Therefore, the "casting" Ignore method has been re-adapted slightly to enable returning the original task at the end, so that it can successfully be cast back after all Ignore calls, by the last TResult-based Ignore call. The code above has been amended to correct this scenario. This does not make the entire pattern particularly elegant but at least it seems to be working properly now.

Vector Sigma
  • 194
  • 1
  • 4
  • 16
  • 1
    I actually realized this possibility earlier, when I read *I would rely on task chaining* in Vladimir's answer. I tried it and surprisingly it worked! Thanks for pointing it out. :-) – Theodor Zoulias Oct 20 '19 at 01:46
  • IMHO, going with your proposed solution and chaining the calls is your best bet. Much inventive and effective as the rest of the answers are, you would simply be complicating your code for no sufficiently good reason. – Vector Sigma Oct 20 '19 at 01:52
  • Regarding the `Task` version, it is much less elegant not only for the reason you mentioned, but also because the `TResult` cannot be inferred and must be repeated in every `Ignore`. Unfortunately C# does not support [partial type inference](https://stackoverflow.com/questions/4003552/partial-type-inference). – Theodor Zoulias Oct 20 '19 at 01:54
  • Unfortunately, yes. At least, you get a consolation prize (though one you may never need, however), see the updated answer. – Vector Sigma Oct 20 '19 at 01:59
  • Yeap, this is a bonus indeed. It is a bit evil though, because it makes it alluring to use the different default values as special error codes, which is not a good practice. :-) – Theodor Zoulias Oct 20 '19 at 02:03
  • If you are prepared to sacrifice compile-time type checks (only when you need to ignore **multiple exception types** _and_ return a default value), you can ignore the exception types using your own extension methods and call the "casting" extension method as the last of the chain. – Vector Sigma Oct 20 '19 at 02:37
  • The last attempt fails for me with the exception: `System.InvalidCastException: Unable to cast object of type 'System.Threading.Tasks.Task'1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'1[System.Int32]'.` – Theodor Zoulias Oct 20 '19 at 04:03
  • 1
    Indeed, this clearly reveals the behavior of async/await, which _seems_ to be swallowing the awaitable parameter and returning a new Task of its own (haven't tested this directly, e.g. with `GetHashCode`, but it appears that this is the case). I have changed the code to use a workaround. In any case, your [final proposition](https://stackoverflow.com/a/58471802/10102452) appears to be the most clear and concise way to achieve the purpose of your question. – Vector Sigma Oct 20 '19 at 17:54
  • Your workaround is a nice try! Unfortunately it works only with the last chained exception type. The previous exception types are not ignored. My own solution, although it works, it looks a bit over-engineered. Ingoring some exceptions should not be so complicated! – Theodor Zoulias Oct 20 '19 at 19:54
1

As pointed out in Vector Sigma's answer, it is possible to chain my original one-type methods to achieve ignoring multiple types of exceptions. Chaining the Ignore for Task<TResult> is quite awkward though, because of the required repetition of the TResult type and the defaultValue. After reading the accepted answer of a question about partial type inference, I figured out how to fix this. I need to introduce a generic task-wrapper struct that will hold this state, and contain a chainable method Ignore. This is the intended usage:

var result = await myInt32Task.WithDefaultValue(0)
    .Ignore<OperationCanceledException>()
    .Ignore<TimeoutException>();

Here is the task-wrapper that I named TaskWithDefaultValue, and the extension method WithDefaultValue.

public readonly struct TaskWithDefaultValue<TResult>
{
    private readonly Task<TResult> _task;
    private readonly TResult _defaultValue;

    public TaskWithDefaultValue(Task<TResult> task, TResult defaultValue)
    {
        _task = task;
        _defaultValue = defaultValue;
    }

    public Task<TResult> GetTask() => _task;
    public TaskAwaiter<TResult> GetAwaiter() => _task.GetAwaiter();

    public TaskWithDefaultValue<TResult> Ignore<TException>()
        where TException : Exception
    {
        var continuation = GetContinuation(_task, _defaultValue);
        return new TaskWithDefaultValue<TResult>(continuation, _defaultValue);

        async Task<TResult> GetContinuation(Task<TResult> t, TResult dv)
        {
            try
            {
                return await t.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                if (ex is TException) return dv;
                throw;
            }
        }
    }
}

public static TaskWithDefaultValue<TResult> WithDefaultValue<TResult>(
    this Task<TResult> task, TResult defaultValue)
{
    return new TaskWithDefaultValue<TResult>(task, defaultValue);
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I simplified the implementation of `Ignore` by replacing the `ContinueWith` with an asynchronous [local function](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions). – Theodor Zoulias Oct 20 '19 at 19:37