I am writting an API that has a ValueTask<T>
return type, and accepts a CancellationToken
. In case the CancellationToken
is already canceled upon invoking the method, I would like to return a canceled ValueTask<T>
(IsCanceled == true
), that propagates an OperationCanceledException
when awaited. Doing it with an async method is trivial:
async ValueTask<int> MyMethod1(CancellationToken token)
{
token.ThrowIfCancellationRequested();
//...
return 13;
}
ValueTask<int> task = MyMethod1(new CancellationToken(true));
Console.WriteLine($"IsCanceled: {task.IsCanceled}"); // True
await task; // throws OperationCanceledException
I decided to switch to a non-async implementation, and now I have trouble reproducing the same behavior. Wrapping a Task.FromCanceled
results correctly to a canceled ValueTask<T>
, but the type of the exception is TaskCanceledException
, which is not desirable:
ValueTask<int> MyMethod2(CancellationToken token)
{
if (token.IsCancellationRequested)
return new ValueTask<int>(Task.FromCanceled<int>(token));
//...
return new ValueTask<int>(13);
}
ValueTask<int> task = MyMethod2(new CancellationToken(true));
Console.WriteLine($"IsCanceled: {task.IsCanceled}"); // True
await task; // throws TaskCanceledException (undesirable)
Another unsuccessful attempt is to wrap a Task.FromException
. This one propagates the correct exception type, but the task is faulted instead of canceled:
ValueTask<int> MyMethod3(CancellationToken token)
{
if (token.IsCancellationRequested)
return new ValueTask<int>(
Task.FromException<int>(new OperationCanceledException(token)));
//...
return new ValueTask<int>(13);
}
ValueTask<int> task = MyMethod3(new CancellationToken(true));
Console.WriteLine($"IsCanceled: {task.IsCanceled}"); // False (undesirable)
await task; // throws OperationCanceledException
Is there any solution to this problem, or I should accept that my API will behave inconsistently, and sometimes will propagate TaskCanceledException
s (when the token is already canceled), and other times will propagate OperationCanceledException
s (when the token is canceled later)?
Update: As a practical example of the inconsistency I am trying to avoid, here is one from the built-in Channel<T>
class:
Channel<int> channel = Channel.CreateUnbounded<int>();
ValueTask<int> task1 = channel.Reader.ReadAsync(new CancellationToken(true));
await task1; // throws TaskCanceledException
ValueTask<int> task2 = channel.Reader.ReadAsync(new CancellationTokenSource(100).Token);
await task2; // throws OperationCanceledException
The first ValueTask<int>
throws a TaskCanceledException
, because the token is already canceled. The second ValueTask<int>
throws an OperationCanceledException
, because the token is canceled 100 msec later.