Reputation: 43836
I encountered a strange behavior while writing some complex async/await code. I managed to create accidentally a canceled Task
with a dual (schizophrenic) identity. It can either throw a TaskCanceledException
or an OperationCanceledException
, depending on how I wait it.
Wait
throws an AggregateException
, that contains a TaskCanceledException
.await
throws an OperationCanceledException
.Here is a minimal example that reproduces this behavior:
var canceledToken = new CancellationToken(true);
Task task = Task.Run(() =>
{
throw new OperationCanceledException(canceledToken);
});
try { task.Wait(); } // First let's Wait synchronously the task
catch (AggregateException aex)
{
var ex = aex.InnerException;
Console.WriteLine($"task.Wait() failed, {ex.GetType().Name}: {ex.Message}");
}
try { await task; } // Now let's await the same task asynchronously
catch (Exception ex)
{
Console.WriteLine($"await task failed, {ex.GetType().Name}: {ex.Message}");
}
Console.WriteLine($"task.Status: {task.Status}");
Output:
task.Wait() failed, TaskCanceledException: A task was canceled.
await task failed, OperationCanceledException: The operation was canceled.
task.Status: Canceled
Can anyone explain why is this happening?
P.S. I know that the TaskCanceledException
derives from the OperationCanceledException
. Still I don't like the idea of exposing an async API that demonstrates such a weird behavior.
Variants: The task below has a different behavior:
Task task = Task.Run(() =>
{
canceledToken.ThrowIfCancellationRequested();
});
This one completes in a Faulted
state (instead of Canceled
), and propagates an OperationCanceledException
with either Wait
or await
.
This is quite puzzling because the CancellationToken.ThrowIfCancellationRequested
method does nothing more than throwing an OperationCanceledException
, according to the source code!
Also the task below demonstrates yet another different behavior:
Task task = Task.Run(() =>
{
return Task.FromCanceled(canceledToken);
});
This task completes as Canceled
, and propagates a TaskCanceledException
with either Wait
or await
.
I have no idea what's going on here!
Upvotes: 2
Views: 1325
Reputation: 1063774
Summarising multiple comments, but:
Task.Wait()
is a legacy API that pre-dates await
.Wait()
would manifest cancellation as TaskCanceledException
; to preserve backwards compatibility, .Wait()
intervenes here, to expose all OperationCanceledException
faults as TaskCanceledException
, so that existing code continues to work correctly (in particular, so that existing catch (TaskCanceledException)
handlers continue to work)await
uses a different API; .GetAwaiter().GetResult()
, which behaves more like you would expect and want (although it is not expected to be used until the task is known to have completed), BUT!.Wait()
or .GetAwaiter().GetResult()
, preferring await
in almost all cases - see "sync over async".Wait()
or GetAwaiter().GetResult()
, then you are stepping into danger, and any consequences are now entirely your fault as the consumer; this is not something that a library author can, or should, compensate for (other than providing twin synchronous and asynchronous APIs)Task[<T>]
, this pattern with various other awaitables would be an undefined behaviour (to be honest, I'm not sure it is really "defined" for Task[<T>]
)await
(but equally, only await
once; using await
multiple times is also an undefined behaviour for awaitables other than Task[<T>]
)catch (OperationCanceledException)
over catch (TaskCanceledException)
, since the former will handle both via inheritanceUpvotes: 4