The following code creates a task which is being canceled. await
expression (case 1) throws System.OperationCanceledException
while synchronous Wait()
(case 2) throws System.Threading.Tasks.TaskCanceledException
(wrapped in System.AggregateException
).
using System; using System.Threading; using System.Threading.Tasks; public class Program { public static void Main() { Program.MainAsync().Wait(); } private static async Task MainAsync() { using(var cancellationTokenSource = new CancellationTokenSource()) { var token = cancellationTokenSource.Token; const int cancelationCheckTimeout = 100; var task = Task.Run( async () => { for (var i = 0; i < 100; i++) { token.ThrowIfCancellationRequested(); Console.Write("."); await Task.Delay(cancelationCheckTimeout); } }, cancellationTokenSource.Token ); var cancelationDelay = 10 * cancelationCheckTimeout; cancellationTokenSource.CancelAfter(cancelationDelay); try { await task; // (1) //task.Wait(); // (2) } catch(Exception ex) { Console.WriteLine(ex.ToString()); Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}"); Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}"); Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}"); } } } }
Case 1 output:
..........System.OperationCanceledException: The operation was canceled. at System.Threading.CancellationToken.ThrowIfCancellationRequested() at Program.<>c__DisplayClass1_0.<<MainAsync>b__0>d.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Program.<MainAsync>d__1.MoveNext() Task.IsCanceled: True Task.IsFaulted: False Task.Exception: null
Case 2 output:
..........System.AggregateException: One or more errors occurred. ---> System.Threading.Tasks.TaskCanceledException: A task was canceled. --- End of inner exception stack trace --- at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) at System.Threading.Tasks.Task.Wait() at Program.<MainAsync>d__1.MoveNext() ---> (Inner Exception #0) System.Threading.Tasks.TaskCanceledException: A task was canceled.<--- Task.IsCanceled: True Task.IsFaulted: False Task.Exception: null
Why System.AggregateException
in the 2nd case doesn't contain System.OperationCanceledException
as an inner exception?
I know that ThrowIfCancellationRequested()
throws OperationCanceledException
and we can see that in both cases Task
gets to canceled (not faulty) state.
This puzzles me because canceling a method from .NET API produces consistent behaviour in both cases - canceled task throws only TaskCanceledException
:
using System; using System.Threading; using System.Threading.Tasks; public class Program { public static void Main() { Program.MainAsync().Wait(); } private static async Task MainAsync() { using(var cancellationTokenSource = new CancellationTokenSource()) { var token = cancellationTokenSource.Token; var task = Task.Delay(1000, token); cancellationTokenSource.CancelAfter(100); try { await task; // (1) //task.Wait(); // (2) } catch(Exception ex) { Console.WriteLine(ex.ToString()); Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}"); Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}"); Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}"); } } } }
Case 1 output:
System.Threading.Tasks.TaskCanceledException: A task was canceled. at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Program.<MainAsync>d__1.MoveNext() Task.IsCanceled: True Task.IsFaulted: False Task.Exception: null
Case 2 output:
System.AggregateException: One or more errors occurred. ---> System.Threading.Tasks.TaskCanceledException: A task was canceled. --- End of inner exception stack trace --- at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) at System.Threading.Tasks.Task.Wait() at Program.<MainAsync>d__1.MoveNext() ---> (Inner Exception #0) System.Threading.Tasks.TaskCanceledException: A task was canceled.<--- Task.IsCanceled: True Task.IsFaulted: False Task.Exception: null
Instead of canceling your running task and catching exception to show user what has really happened, you can continue with another task only if the previous task has been cancelled. Make use of Continue With method which takes the returned value from the cancelled task as a parameter.
There's 2 likely reasons that a TaskCanceledException would be thrown: Something called Cancel() on the CancellationTokenSource associated with the cancellation token before the task completed. The request timed out, i.e. didn't complete within the timespan you specified on HttpClient. Timeout .
For canceling, we use CancellationTokenSource object.
Create and start a cancelable task. Pass a cancellation token to your user delegate and optionally to the task instance. Notice and respond to the cancellation request in your user delegate. Optionally notice on the calling thread that the task was canceled.
The difference here comes from using token.ThrowIfCancellationRequested()
. This method checks for cancellation and if requested throws OperationCanceledException
specifically and not TaskCanceledException
(understandable as CancellationToken
isn't exclusive to the TPL). You can look at the reference source and see that it calls this method:
private void ThrowOperationCanceledException() { throw new OperationCanceledException(Environment.GetResourceString("OperationCanceled"), this); }
"Regular" cancellation though will indeed generate a TaskCanceledException
. You can see that by cancelling the token before the task had a chance to start running:
cancellationTokenSource.Cancel(); var task = Task.Run(() => { }, cancellationTokenSource.Token); try { await task; } catch (Exception ex) { Console.WriteLine(ex.ToString()); Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}"); Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}"); Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}"); }
Output:
System.Threading.Tasks.TaskCanceledException: A task was canceled. at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at Sandbox.Program.<MainAsync>d__1.MoveNext() Task.IsCanceled: True Task.IsFaulted: False Task.Exception: null
Traditional .Net methods usually don't use CancellationToken.ThrowIfCancellationRequested
for async API as this is only appropriate when offloading work to another thread. These methods are for inherently asynchronous operations so cancellation is monitored using CancellationToken.Register
(or the internal InternalRegisterWithoutEC
).
TaskCanceledException
inherits from OperationCanceledException
. So it least there is a little consitency.
if( ex is OperationCanceledException) { ... }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With