Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle exceptions when using Task Parallel Library Task.WhenAny()

When I use the Task.WhenAll() function and an exception is thrown in a Task a new AggregateException is thrown, I can catch it to see all the exceptions that occurred in the Tasks. However, when I use Task.WhenAny() no exception is thrown. Instead, I have to check the Task.Exception property for a value to see if an exception occurred. This seems like a bad code smell as I would have to remember to check the Task.Exception property every I use Task.WhenAny(). Shouldn't there be a better way?

Here's an example of what I mean:

private async void btnMultipleExceptions_Click(object sender, EventArgs e) {
        var task1 = ThrowNotImplementedException();
        var task2 = ThrowDivideByZeroException();

        try {
            Task task = await Task.WhenAny(task1, task2);

            // Even if an exception is thrown in one of the tasks (in our case,
            // task1 will throw first) no exception is thrown from
            // the above await Task.WhenAny(). Instead, the exception is placed on the 
            // Task.Exception property. So I need to check for it every time 
            // I call Task.WhenAny()?
            if (task.Exception != null) {
                Console.WriteLine("Exceptions: " + string.Join(Environment.NewLine,
                    task.Exception.InnerExceptions.Select(x => x.Message).ToArray()));
            } else {
                Console.WriteLine("No Exceptions!");
            }
        } catch(Exception ex) {
            // Try to catch all exceptions???
            AggregateException allEx = ex as AggregateException;

            if (allEx != null) {
                Console.WriteLine("Exceptions: " + string.Join(Environment.NewLine,
                    allEx.InnerExceptions.Select(x => x.Message).ToArray()));
            } else {
                Console.WriteLine("Exceptions: " + ex.Message);
            }
        }
    }

    private async Task ThrowNotImplementedException() {
        await Task.Delay(TimeSpan.FromSeconds(1));
        throw new NotImplementedException();
    }

    private async Task ThrowDivideByZeroException() {
        await Task.Delay(TimeSpan.FromSeconds(2));
        throw new DivideByZeroException();
    }
like image 701
Henry Daehnke Avatar asked May 22 '15 17:05

Henry Daehnke


People also ask

How do you handle exceptions thrown by tasks?

You can also handle the original exceptions by using the AggregateException. Handle method. Even if only one exception is thrown, it is still wrapped in an AggregateException exception, as the following example shows. public static partial class Program { public static void HandleThree() { var task = Task.

How to throw exception in Task c#?

try { t. Start(); await t; } catch (Exception e) { // When awating on the task, the exception itself is thrown. // in this case a regular Exception. } } In TPL, When throwing an exception inside a Task, it's wrapped with an AggregateException.

What is unobserved task exception?

An “unobserved” exception is one that's stored into the task but then never looked at in any way by the consuming code. There are many ways of observing the exception, including Wait()'ing on the Task, accessing a Task<TResult>'s Result, looking at the Task's Exception property, and so on.

What is AggregateException C#?

AggregateException is used to consolidate multiple failures into a single, throwable exception object. It is used extensively in the Task Parallel Library (TPL) and Parallel LINQ (PLINQ). For more information, see Exception Handling and How to: Handle Exceptions in a PLINQ Query.


2 Answers

Simply await the task returned from WhenAny. If it's faulted, it'll unwrap the exception and throw it, if it didn't, you know it's already done, so you can continue on.

Alternatively, you could simply call Unwrap on Task.WhenAny and then await that. This would be semantically identical to the previous option; it's only difference is in whether you think this is more or less clear.

If you find yourself very frequently unwrapping the result of WhenAny you could simply write your own implementations that unwrap the task for you, and use those instead:

public static Task WhenAny(IEnumerable<Task> tasks)
{
    return Task.WhenAny(tasks).Unwrap();
}
public static Task<T> WhenAny<T>(IEnumerable<Task<T>> tasks)
{
    return Task.WhenAny(tasks).Unwrap();
}
//TODO add wrappers for the `params` overloads if you want them too
like image 68
Servy Avatar answered Oct 03 '22 03:10

Servy


While the accepted answer will give you the exception from the Task that completed first. The exception in the 2nd task will still be thrown, and in versions of .NET prior to 4.5 it will be escalated at the thread level when it gets GC'd if it goes unobserved. Observing and logging non-fatal task exceptions is an excellent use for Continuations (I assume from your WhenAny() scenario that it's a non-fatal condition if one of your tasks fails). Consider something like this:

private static void LogIfErrors(Task source)
{
    if(source.Exception == null) return;
    source.Exception.Handle(ex =>
    {
        Log.Error("#unhandled #task #error", ex);
        return true;
    });
    return;
}

private void DoStuff()
{
    // note that you cannot inline the ContinueWith() statement,
    // because it would change the value of task1 to hold your
    // continuation instead of your parent task

    var task1 = ThrowNotImplementedException();
    task1.ContinueWith(LogIfErrors, TaskContinuationOptions.OnlyOnFaulted);

    var task2 = ThrowDivideByZeroException();
    task2.ContinueWith(LogIfErrors, TaskContinuationOptions.OnlyOnFaulted);

    var firstCompleted = await Tasks.WhenAny(task1, task2).Unwrap();

}

Now even if one of your tasks is long-running and the exception is thrown long after your first-to-finish task has been returned by WhenAny(), you will still have the chance to observe and the exception and (in <= .NET 4.0) prevent it from killing the joined thread.

like image 20
Dusty Avatar answered Oct 03 '22 02:10

Dusty