TaskCanceledException using Task <string>
An overview of what I am doing: In a loop, I start a new one Task<string>
and add it to List<Task<string>>
. The problem is that after the row is returned, the task is thrown System.Threading.Tasks.TaskCanceledException
and I don't know why. Below is a summary version of what I am doing.
public async Task<string> GenerateXml(object item)
{
using (var dbContext = new DatabaseContext())
{
//...do some EF dbContext async calls here
//...generate the xml string and return it
return "my xml data";
}
}
var tasks = new List<Task<string>>();
My loop looks like this:
foreach (var item in items)
{
tasks.Add(Task.Run(() => GenerateXml(item).ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
//also tried:
tasks.Add(Task.Run(async () => await GenerateXml(item).ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
//both generate the same exception
//after looking at my code, I was using the ContinueWith on the GenerateXml method call, which should still work, right?
//I moved the continue with to the `Task.Run` and still get the exception.
}
Task.WaitAll(tasks.ToArray()); //this throws the AggregateException which contains the TaskCanceledException
When I go through the code it gets caught in return "my xml data";
, but an exception is thrown.
What I am trying to avoid with ContinueWith
is when I loop over each task and get results, it does not throw the same AggregateException
that it selected with WaitAll
.
Here is a work console app that throws ... I know the problem is related to ContinueWith
, but why?
class Program
{
static void Main(string[] args)
{
var program = new Program();
var tasks = new List<Task<string>>();
tasks.Add(Task.Run(() => program.GenerateXml().ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
Task.WaitAll(tasks.ToArray()); //this throws the AggregateException
foreach (var task in tasks)
{
Console.WriteLine(task.Result);
}
Console.WriteLine("finished");
Console.ReadKey();
}
public async Task<string> GenerateXml()
{
System.Threading.Thread.Sleep(3000);
return "my xml data";
}
}
source to share
You are doing the second task.
.ContinueWith ((t)).
To run the correct one, you need to refactor your code.
Split the line like this:
Task<string> t1 = Task.Run(() => program.GenerateXml());
t1.ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted);
tasks.Add(t1);
You can reorganize tasks like this: (for error handling)
tasks.Add(program.GenerateXml().ContinueWith(t => {return t.IsFaulted? "": t.Result; }));
source to share
As consultant Avram points out, you get an exception because your list does not contain tasks that are performed by a method GenerateXml()
, but those that are continuation of tasks that perform that method.
Since these tasks only run when they GenerateXml()
throw an exception, if any call GenerateXml()
succeeds, then at least one of these continuation tasks will not run. Instead, it completes by canceling (i.e., when its antecedent succeeds) and so the call WaitAll()
sees that it cancels and throws a rolling exception.
IMHO, the best way to solve this problem is to stick with the async
/ pattern await
. That is, instead of using it ContinueWith()
directly, write the code so that it is readable and expressive. In this case, I would write a wrapper method async
to invoke the method GenerateXml()
, catching any exception thrown, and returning a value ""
in that case.
Here's a modified version of your MCVE to show what I mean:
class Program
{
static void Main(string[] args)
{
var tasks = new List<Task<string>>();
tasks.Add(SafeGenerateXml());
Task.WaitAll(tasks.ToArray());
foreach (var task in tasks)
{
Console.WriteLine(task.Result);
}
Console.WriteLine("finished");
Console.ReadKey();
}
static async Task<string> SafeGenerateXml()
{
try
{
return await GenerateXml();
}
catch (Exception)
{
return "";
}
}
static async Task<string> GenerateXml()
{
await Task.Delay(3000);
return "my xml data";
}
}
IMHO this is much more in line with the new idioms async
in C #, much less prone to failure, and much easier to understand what exactly is going on (i.e. avoiding ContinueWith()
altogether, you don't even have the opportunity to get confused about what tasks (tasks) are waiting for like you obviously done in their source code).
source to share
Since you are using .ContinueWith
as a way to recover from an exception, you can simply add a statement try {} catch
.
var tasks = new List<Task<string>>();
foreach (var item in items)
{
var closure = item;
var task =
Task.Factory.StartNew(
async () =>
{
try
{
return await GenerateXml(closure);
}
catch (Exception exception)
{
//log
return "";
}
}).Unwrap();
tasks.Add(task);
}
Task.WaitAll(tasks.ToArray());
However, if I were you, I would hide this logic in a method GenerateXml
. As long as you consider a valid default (empty string here) it should be fine.
var tasks = items.Select(item => Task.Run(() => GenerateXml(item))).ToList();
source to share