A simpler way of ignoring specific types of exceptions when awaiting a Task

c# fire and forget task
c# async fire and forget
async await exception handling c#
c# call async method without await
c# call synchronous method asynchronously
when not to use async await c#
c# return task without await
task.run without await

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?

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.

Fire and Forget approach, For ideas about how to ignore exceptions of tasks, take a look a this: A simpler way of ignoring specific types of exceptions when awaiting a  In just about all situations where you put a try/catch around a block of code, you should always have an appropriate catch handler that captures a specific exception like (UnAuthorizedAccessException ex) - or even a non-specific exception like (Exception ex) to pass on or otherwise handle the exception in the appropriate location.

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);
}

Essential C# 7.0, Unwrap(), thereby shedding the outer Task and appropriately handling any errors earlier in the chapter, there are multiple ways to handle these exceptions: 1. a specific type or conditionally check for the type of the exception separately We can ignore all exception handling from within WriteWebRequestSizeAsync()  In most cases, that means awaiting it, although there are occasions where you might keep hold of the Task to be awaited later. In this example, we call Task.Delay but because we don't await it, the "After" message will get immediately written, because Task.Delay(1000) simply returns a task that will complete in one second, but nothing is

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.

Async in C# 5.0, If you want to find out about all exceptions that happen in your program, you need to make sure that every single Task you make is awaited, or exceptions can get lost. other Tasks is equivalent to catching all exceptions and ignoring them, which is bad practice and tends to The return type of WhenAny is Task<Task<T​>>. In an ideal situation, you would catch all possible types of exception your method could generate, handle them on a per-exception basis, and in the end add a general catch clause to catch any future or unknown exceptions. This way you get the best of both worlds.

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);
}

C# 5.0 Unleashed: C 5.0 Unleashed, This constraint was removed from EventHandler<T>, enabling this simple Luckily, the Progress<T> type addresses this by capturing the current Later in this chapter, when discussing the await keyword, we come back to the topic of In the world of the APM, exceptions can come out during Begin and End method calls. If a method that returns a Task or Task<TResult> throws an exception, the exception is stored in the returned task. The exception is rethrown when the task is awaited. Therefore, make sure that any async method that can produce an exception has a return type of Task or Task<TResult> and that calls to the method are awaited.

Consuming the Task-based Asynchronous Pattern, When you're awaiting a Task, the await expression is of type void . This callback resumes the asynchronous method at the point of suspension. A Task can fault as a result of multiple exceptions, but only one of these may have a different type) and to support a uniform set of generic tasks (such as  try – A try block is used to encapsulate a region of code. If any code throws an exception within that try block, the exception will be handled by the corresponding catch. catch – When an exception occurs, the Catch block of code is executed. This is where you are able to handle the exception, log it, or ignore it.

Async/await tips and tricks • Particular Software, public async Task Handle(DoSomething message, keyword, an exception raised within that method may be silently ignored. or you can use IL weaving so you don't have to type it manually at all. Smart developers use tooling to make writing asynchronous code as simple and smooth as possible. If the exception to catch was thrown with failwith (e.g. a System.Exception) or a custom F# exception, you can match using the simple tag approach shown above. On the other hand, to catch a specific .NET exception class, you have to match using the more complicated syntax:

C# Async Antipatterns, The async and await keywords have done a great job of simplifying In this article, I want to highlight a bunch of the most common async you call a method that returns a Task or Task<T> you should not ignore caution, and make sure the method has good exception handling. NET would be simpler. Arbitrary Static Task Graph (structural pattern) – Addresses the overall organization of the program rather than an implementation on specific programming model(s). Task Decomposition, Order Tasks & Group Tasks – The task decomposition, order tasks, and group tasks patterns detail an approach to understanding task dependencies. They help

Comments
  • 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?
  • @TheodorZoulias Of course. 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!
  • @VectorSigma Yeap, I will probably combine the two answers to get the best of both. :-)
  • @TheodorZoulias I didn't realise your solution had the extra behaviour of ignoring subtypes, I updated my code to do that as well.
  • Now it is excellent! My problem now is that I have too many good answers to choose from. :-)
  • Thanks Vladimir for the answer. For some reason this method causes a TaskCanceledException when the main task completes successfully!
  • Fixed - the continuation with option TaskContinuationOptions.OnlyOnFaulted couldn't be executed on the completed antecedent task that cause this exception.
  • 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<int> GetAsync() => throw new OperationCanceledException(); I am awaiting it like this: await GetAsync().Ignore(0, typeof(OperationCanceledException)); and it throws.
  • 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.
  • 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.