theateist
theateist

Reputation: 14399

Throwing any exception from Task throws a TaskCanceledException

I never call cts.Cancel(), so why does it throw TaskCanceledException exception?

var cts = new CancellationTokenSource();
try
{
    await Task.Factory.StartNew(() =>
    {
        throw new Exception("");                      
    }, cts.Token)
        .ContinueWith(t =>
        {           
        }, TaskContinuationOptions.OnlyOnCanceled);
}
catch (TaskCanceledException ex)
{
    throw;
}

Upvotes: 1

Views: 1042

Answers (3)

Stephen Cleary
Stephen Cleary

Reputation: 456427

Others have noted why this is happening: as documented, the continuation task is canceled if the conditions specified in ContinueWith are not met.

If the criteria specified through the continuationOptions parameter are not met, the continuation task will be canceled instead of scheduled.

The best way to avoid this problem is not to use ContinueWith at all. You should also not use StartNew or TaskCanceledException. Use await instead of ContinueWith, Task.Run instead of Task.Factory.StartNew, and OperationCanceledException instead of TaskCanceledException.

Your resulting code will be more correct and also shorter, cleaner, and easier to maintain.

Upvotes: 3

Arthur JF
Arthur JF

Reputation: 111

It throws TaskCancelledException because of the Chained Task (task inside ContinueWith Method). Once you throw an exception inside your code, the task finishes with faulted result and starts the second task. When you use await (await Task.Factory ...) you await the SECOND task, which is cancelled because the previous task was failed (not cancelled), therefore, you get the task cancelled exception.

Await will throw any existing exception in the chain in to the calling thread. So you are getting the last exception in the chain (from the second task).

You can see in this example what code is executing in the test cases based on your code. Please note I removed the async keyword before ...StartNew(() => $ ...., and that CTS on Task.Run / Task.StartNew only prevents task for starting but wont stop the task unless you directly check for cancellation inside Task body.

Check the reference: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/

<pre>
var cases = new[] { 
    "Task normally run", 
    "Task with cancellation timedout before run", 
    "Task with cts timing out during run", 
    "Task with cancellation inside method (calling Cancel())" 
};

var cancellationTimeout = new[] { 3000, 1, 500, 3000 };
var delay = new[] { 0, 500, 0, 0 };
var cancel = new[] { false, false, false, true };

for (int i = 0; i < cancellationTimeout.Length; i++)
    await test(i);

async Task test(int i)
{
    Console.WriteLine($"Test case {i + 1}. {cases[i]}");
    Console.WriteLine($"    Cancellation Timeout: {cancellationTimeout[i]} ms, \n    Delay Start: {delay[i]} ms, \n    Cancel task 1: {cancel[i]}, \n    Task 1 duration: 750ms");
    Console.WriteLine($"=== Console log ===");

    var cts = new CancellationTokenSource(cancellationTimeout[i]);

    try
    {
        await Task.Delay(delay[i]);

        var t1 = Task.Factory.StartNew(() =>
        {
            Console.WriteLine("         Task 1 started");
            if (cancel[i])
            {
                Console.WriteLine("         This is executed if the cancellation token is requested");
                cts.Cancel();
                Console.WriteLine("         This is executed when you check for cancellation");
                cts.Token.ThrowIfCancellationRequested();
                Console.WriteLine("         This is executed after throw");
            }
            Task.Delay(500).Wait();
            Console.WriteLine("         Task 1 finished");
        }, cts.Token); //This canellation token only works if the current task has not started and cancellation request is .

        var t2 = t1.ContinueWith(t =>
        {
            Console.WriteLine("         This is executed if the task 1 is cancelled and handled");
        }, TaskContinuationOptions.OnlyOnCanceled);


        Exception? Ex1 = null;
        try
        {
            await t1;
        }
        catch (Exception ex) { Ex1 = ex; }

        Exception? Ex2 = null;
        try
        {
            await t2;
        }
        catch (Exception ex) { Ex2 = ex; }

        Console.WriteLine($"=== Tasks ===");
        Console.WriteLine($"     Task 1 status:{t1.Status,20}    Faulted?:{t1.IsFaulted,10}    Ex:{Ex1?.Message}");
        Console.WriteLine($"     Task 2 status:{t2.Status,20}    Faulted?:{t2.IsFaulted,10}    Ex:{Ex2?.Message}");
        Console.WriteLine("");


    }
    catch (TaskCanceledException ex)
    {
        throw;
    }
}
</pre>

You can check the following results.

Test case 1. Task normally run
    Cancellation Timeout: 3000 ms,
    Delay Start: 0 ms,
    Cancel task 1: False,
    Task 1 duration: 750ms
=== Console log ===
         Task 1 started
         Task 1 finished
=== Tasks ===
     Task 1 status:     RanToCompletion    Faulted?:     False    Ex:
     Task 2 status:            Canceled    Faulted?:     False    Ex:A task was canceled.

Test case 2. Task with cancellation timedout before run
    Cancellation Timeout: 1 ms,
    Delay Start: 500 ms,
    Cancel task 1: False,
    Task 1 duration: 750ms
=== Console log ===
         This is executed if the task 1 is cancelled and handled
=== Tasks ===
     Task 1 status:            Canceled    Faulted?:     False    Ex:A task was canceled.
     Task 2 status:     RanToCompletion    Faulted?:     False    Ex:

Test case 3. Task with cancellation timeout during run
    Cancellation Timeout: 500 ms,
    Delay Start: 0 ms,
    Cancel task 1: False,
    Task 1 duration: 750ms
=== Console log ===
         Task 1 started
         Task 1 finished
=== Tasks ===
     Task 1 status:     RanToCompletion    Faulted?:     False    Ex:
     Task 2 status:            Canceled    Faulted?:     False    Ex:A task was canceled.

Test case 4. Task with cancellation inside method
    Cancellation Timeout: 3000 ms,
    Delay Start: 0 ms,
    Cancel task 1: True,
    Task 1 duration: 750ms
=== Console log ===
         Task 1 started
         This is executed if the cancellation token is requested
         This is executed when you check for cancellation
         This is executed if the task 1 is cancelled and handled
=== Tasks ===
     Task 1 status:            Canceled    Faulted?:     False    Ex:The operation was canceled.
     Task 2 status:     RanToCompletion    Faulted?:     False    Ex:

Good Luck

Upvotes: 1

thug_
thug_

Reputation: 1093

TaskCanceledException is thrown because TaskContinuationOptions.OnlyOnCanceled option is set for the continuation task, which causes the continuation task to throw a TaskCanceledException if the antecedent task (the initial task) throws any exception, regardless of whether the antecedent task was actually canceled.

Upvotes: 0

Related Questions