Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TaskCanceledException using Task<string>

Tags:

c#

An overview of what I'm doing: In a loop, I'm starting a new Task<string> and adding it to a List<Task<string>>. The issue is, after the string is returned, the task throws a System.Threading.Tasks.TaskCanceledException and I don't know why. Below is a trimmed version of what I'm 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:

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 step through the code, it hits the return "my xml data"; but the exception is thrown.

What I'm trying to avoid with ContinueWith is when I loop each task and get the results, it doesn't throw the same AggregateException that it threw with WaitAll.

Here is a working console app that throws... I know the issue is with the 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";
    }
}
like image 395
Jacob Roberts Avatar asked May 11 '26 23:05

Jacob Roberts


2 Answers

As answerer Avram hints at, you are getting the exception because your list contains not the tasks that are running the GenerateXml() method, but rather those that are the continuations of the tasks running that method.

Since those tasks get run only when GenerateXml() throws an exception, if any call to GenerateXml() succeeds, then at least one of those continuation tasks won't get run. Instead, it's completed by being cancelled (i.e. when its antecedent task completes successfully), and so the call to WaitAll() sees that cancellation and throws the aggregate exception.

IMHO, the best way to address this is to stick with the async/await pattern. I.e. rather than using ContinueWith() directly, write the code so that it's readable and expressive. In this case, I would write a wrapper async method to call the GenerateXml() method, catching any exception that occurs, and returning the "" value in that case.

Here's a modified version of your MCVE showing 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 keeping with the new async idioms in C#, much less prone to failure, and is much easier to understand what exactly is going on (i.e. by avoiding ContinueWith() altogether, you don't even have the chance to get confused about which task(s) is(are) being waited on, as you obviously did in your original code).

like image 150
Peter Duniho Avatar answered May 14 '26 15:05

Peter Duniho


You are running the second task.
.ContinueWith((t) task.

To run the correct one, you need to refactor the code.
Split the line like this:

    Task<string> t1 = Task.Run(() => program.GenerateXml());
    t1.ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted);
    tasks.Add(t1);

You can refactor tasks like this: (for error handling)

tasks.Add(program.GenerateXml().ContinueWith(t => {return t.IsFaulted? "": t.Result; }));
like image 20
Avram Avatar answered May 14 '26 13:05

Avram