When catching the Task.WhenAll
AggregateException
the TaskCancelledException
is not available when other tasks are faulted. Running below code I get the following output:
TaskException
Caught in AggregateException
System.ArgumentOutOfRangeException
TaskCanceledException
Caught in TaskCanceledException
TaskException-TaskCanceledException
Caught in AggregateException
System.ArgumentOutOfRangeException
In the last test TaskCancelledException
is not thrown, nor is it in the AggregateException
.
Is there a means to also have the TaskCanceledException
in the AggregateException
, or do I always have to check the tasks exception when other tasks have errors?
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
public static class Program
{
public static void Main()
{
var task1 = Task.FromException(new ArgumentOutOfRangeException());
var task2 = Task.FromCanceled(new CancellationToken(true));
Test("TaskException", new[] { task1 });
Test("TaskCanceledException", new[] { task2 });
Test("TaskException-TaskCanceledException", new Task[] { task1, task2 });
static async void Test(string title, Task[] tasks)
{
Console.WriteLine();
Console.WriteLine(title);
var task = Task.WhenAll(tasks);
try { await task; }
catch (TaskCanceledException)
{
Console.WriteLine($"Caught in TaskCanceledException");
}
catch
{
Console.WriteLine($"Caught in AggregateException");
if (task.Exception is not null)
{
var t = task.Exception.Flatten();
foreach (var x in t.InnerExceptions)
{
Console.WriteLine(x.GetType());
}
}
}
}
}
}
The behavior of the Task.WhenAll
method is to return a canceled Task
in case some of the tasks
are canceled, and a faulted Task
in case some of the tasks
are faulted. In case the tasks
array contains both canceled and faulted tasks, the Task.WhenAll
ignores the canceled tasks, and returns a faulted Task
that contains the exceptions of the faulted tasks.
If you wonder why the Task.WhenAll
behaves this way, you could think that a canceled task contains no exception. Its Exception
property is null
. The TaskCanceledException
emerges only when you await
a canceled task, and the only information that it conveys is the CancellationToken
that caused the cancellation. Any textual information that was present in the original OperationCanceledException
, is lost.
If you want to change the behavior of the Task.WhenAll
so that it treats the canceled tasks as faulted, one idea is to pass the tasks through a converter that changes their completion status. Something like this:
public static Task CanceledToFaulted(Task task)
{
ArgumentNullException.ThrowIfNull(task);
return task.ContinueWith(t =>
{
if (t.IsCanceled)
return Task.FromException(new TaskCanceledException(t));
// In any other case, propagate the task as is.
return t;
}, CancellationToken.None, TaskContinuationOptions.DenyChildAttach |
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default).Unwrap();
}
Usage example:
Task task = Task.WhenAll(tasks.Select(t => CanceledToFaulted(t)));