Task Exception Null in Chained C# Task Continuation - c#

I have a C# application which executes a method that returns a Task. In this case, I'm trying to show a message box containing the exception details if the called method throws an exception.
If I call it like this, I can see the exception fine in task.Exception:
MyClass.MyAsyncMethod(cancellationToken)
.LogExceptions()
.ContinueWith(task => {
MessageBox.Show(task.Exception);
}, cancellationToken, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default);
However, if I add an OnlyOnRanToCompletion continuation, task.Exception in the NotOnRanToCompletion continuation becomes null:
MyClass.MyAsyncMethod(cancellationToken)
.LogExceptions()
.ContinueWith(task => {
Log.Info("Executed");
}, cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default)
.ContinueWith(task => {
MessageBox.Show(task.Exception);
}, cancellationToken, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default);
The code in the OnlyOnranToCompletion continuation is not executed, but the code in the NotOnRanToCompletion continuation is executed and task.Exception is null. Why does this happen?
Note: I cannot use C# >= 5.0 features like async or await. I'm also able to get around this by having it all under a single None continuation method that checks the exception parameter of the task and determines if an exception occured. However, I'm interested in WHY the above behaviour takes place.
This is the called method:
public Task<bool> MyAsyncMethod(CancellationToken cancellationToken)
{
return Task<bool>.Factory.StartNew(() =>
{
...
try
{
var response = request.GetResponse();
if (response.StatusCode != HttpStatusCode.OK)
throw new Exception("Invalid response status code: " + response.StatusCode);
}
catch (Exception ex)
{
Logger.Error("Request failed", ex));
throw;
}
}, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
}

There is no exception because the Task that it is a continuation of didn't throw an exception. The Task that it is a continuation of (the previous call to ContinueWith) will have completed successfully.
If you want to handle both the error and non-error case, you're probably better off just having one continuation and checking if it was successful in that continuation, rather than using the continuation options. Alternatively you could store the Task from the actual work that you're doing and add both continuations as continuations to that one Task, rather than having one continuation being a continuation of the other.

Related

Passing cancellation token to Task.Run seems to have no effect [duplicate]

This question already has an answer here:
Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequested
(1 answer)
Closed last month.
According to this and this, passing a cancellation token to a task constructor, or Task.Run, will cause the task to be associated with said token, causing the task to transition to Canceled instead of Faulted if a cancellation exception occurs.
I've been fiddling with these examples for a while, and I can't see any benefits other than preventing a cancelled task to start.
Changing the code on this MSDN example from
tc = Task.Run(() => DoSomeWork(i, token), token);
to
tc = Task.Run(() => DoSomeWork(i, token));
produced the exact same output:
This code also results in two cancelled state tasks with the same exceptions thrown:
var token = cts.Token;
var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});
var t2 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
}, token);
Console.ReadKey();
try
{
cts.Cancel();
Task.WaitAll(t1, t2);
}
catch(Exception e)
{
if (e is AggregateException)
{
foreach (var ex in (e as AggregateException).InnerExceptions)
{
Console.WriteLine(e.Message);
}
}
else
Console.WriteLine(e.Message);
}
Console.WriteLine($"without token: { t1.Status }");
Console.WriteLine($"with token: { t2.Status }");
Console.WriteLine("Done.");
Apparently, throwing OperationCanceledException from within the task is enough to make it transition to Canceled instead of Faulted. So my question is: is there a reason for passing the token to the task other than preventing a cancelled task to run?
Is there a reason for passing the token to the task other than preventing a cancelled task to run?
In this particular case, No. Past the point in time that the task has started running, the token has no effect to the outcome.
The Task.Run method has many overloads. This case is peculiar because of the infinite while loop.
var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});
The compiler has to choose between these two overloads:
public static Task Run(Action action);
public static Task Run(Func<Task> function);
...and for a reason explained in this question it chooses the later. Here is the implementation of this overload:
public static Task Run(Func<Task?> function, CancellationToken cancellationToken)
{
if (function == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.function);
// Short-circuit if we are given a pre-canceled token
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
// Kick off initial Task, which will call the user-supplied function and yield a Task.
Task<Task?> task1 = Task<Task?>.Factory.StartNew(function, cancellationToken,
TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
// Create a promise-style Task to be used as a proxy for the operation
// Set lookForOce == true so that unwrap logic can be on the lookout for OCEs thrown
// as faults from task1, to support in-delegate cancellation.
UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1,
lookForOce: true);
return promise;
}
The important detail is the lookForOce: true. Let's look inside the UnwrapPromise class:
// "Should we check for OperationCanceledExceptions on the outer task and interpret them
// as proxy cancellation?"
// Unwrap() sets this to false, Run() sets it to true.
private readonly bool _lookForOce;
..and at another point below:
case TaskStatus.Faulted:
List<ExceptionDispatchInfo> edis = task.GetExceptionDispatchInfos();
ExceptionDispatchInfo oceEdi;
if (lookForOce && edis.Count > 0 &&
(oceEdi = edis[0]) != null &&
oceEdi.SourceException is OperationCanceledException oce)
{
result = TrySetCanceled(oce.CancellationToken, oceEdi);
}
else
{
result = TrySetException(edis);
}
break;
So although the internally created Task<Task?> task1 ends up in a Faulted state, its unwrapped version ends up as Canceled, because the type of the exception is
OperationCanceledException (abbreviated as oce in the code).
That's a quite convoluted journey in the history of TPL, with methods introduced at different times and frameworks, in order to serve different purposes. The end result is a little bit of inconsistency, or nuanced behavior if you prefer to say it so. A relevant article that you might find interesting is this: Task.Run vs Task.Factory.StartNew by Stephen Toub.

How to handle task cancellation using ContinueWith?

I've got the following example:
public void Run()
{
var ctc = new CancellationTokenSource();
try
{
DoAsync(ctc).Wait();
Console.WriteLine("Done");
}
catch (AggregateException exception)
{
Console.WriteLine("Inside try-catch block");
Console.WriteLine();
Console.WriteLine(exception);
exception.Handle(ex =>
{
Console.WriteLine(ex.Message);
return true;
});
}
}
private async Task DoAsync(CancellationTokenSource ctc)
{
Console.WriteLine("DoAsync started");
await Task.Run(() =>
Console.WriteLine("DoAsync Run"),
ctc.Token
)
.ContinueWith(antecedent =>
Console.WriteLine("DoAsync Run cancelled"),
TaskContinuationOptions.OnlyOnCanceled
);
Console.WriteLine("DoAsync finished");
}
I've created a method (DoAsync) that does some asynchronous work and can be cancelled at any time.
As you can see Task.Run gets a cancellation token. For this reason I created continuation task with continuationOptions = TaskContinuationOptions.OnlyOnCanceled.
As a result I expected continuation task to be called only when cancellation is requested and in other cases - ignored.
But in my implementation task returned by ContinueWith throws an exception when its antecedent task is not being cancelled:
DoAsync started
DoAsync Run
Inside try-catch block
System.AggregateException...
A task was canceled.
I can fix this by adding another ContinueWith as in the example below:
await Task.Run(() =>
Console.WriteLine("DoAsync Run"),
ctc.Token
)
.ContinueWith(antecedent =>
Console.WriteLine("DoAsync Run cancelled"),
TaskContinuationOptions.OnlyOnCanceled
)
.ContinueWith(antecedent => { });
And this code doesn't throw any exceptions.
But can I handle the cancellation using single ContinueWith properly?
The remarks for ContinueWith specifically state:
If the continuation criteria specified through the continuationOptions parameter are not met, the continuation task will be canceled instead of scheduled.
Since the criteria you specified for the antecedent weren't met, (namely, it wasn't cancelled) the continuation was set to be cancelled. You awaited the cancelled task, which therefore result in DoAsync faulting with an operation cancelled exception.

Exception propagation from Task.StartNew

Consider this code:
public IEnumerable<string> GetAmazonMwsNotifications(ScAmazonNotificationType scAmazonNotificationType, CancellationToken cancellationToken)
{
var scAmazonSqsMwsNotificationsManagmentClientRequestBuilder = _scServiceLocator.GetInstance<IScAmazonSqsMwsNotificationsManagmentClientRequestBuilder>();
var blockingCollection = new BlockingCollection<string>();
try
{
StartReceiveMessagesAsync(blockingCollection, cancellationToken, scAmazonNotificationType, scAmazonSqsMwsNotificationsManagmentClientRequestBuilder);
}
catch (Exception exception)
{
throw //this catch is never called;
}
return blockingCollection.GetConsumingEnumerable(cancellationToken);
}
private async void StartReceiveMessagesAsync(BlockingCollection<string> blockingCollection, CancellationToken cancellationToken, ScAmazonNotificationType scAmazonNotificationType, IScAmazonSqsMwsNotificationsManagmentClientRequestBuilder scAmazonSqsMwsNotificationsManagmentClientRequestBuilder)
{
var semaphore = new SemaphoreSlim(15);
var receiveMessageRequest = scAmazonSqsMwsNotificationsManagmentClientRequestBuilder.BuildReceiveMessageRequest(scAmazonNotificationType);
while (!cancellationToken.IsCancellationRequested)
{
await semaphore.WaitAsync(cancellationToken);
Task.Factory.StartNew(() =>
{
try
{
throw new ApplicationException("Test");
var receiveMessageResponse = _scAmazonSqsClientWrapper.ReceiveMessageAsync(receiveMessageRequest, cancellationToken).Result;
foreach (var result in receiveMessageResponse.Messages.Select(p => p.Body))
{
blockingCollection.Add(result, cancellationToken);
}
var deleteFromQueueRequest = scAmazonSqsMwsNotificationsManagmentClientRequestBuilder.BuildBatchDeleteMessageRequest(scAmazonNotificationType, receiveMessageResponse.Messages.Select(p => p.ReceiptHandle).ToArray());
_scAmazonSqsClientWrapper.DeleteMessageBatchAsync(deleteFromQueueRequest, cancellationToken);
}
finally
{
semaphore.Release(1);
}
}, cancellationToken, TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent, new ThreadPerTaskScheduler());
}
}
If the exception is thrown inside the task delegate it's never propagated to the calling method. I can't await for task inside the semaphore, because in this case semaphore will be blocked by the awaited task. Is there any way to propagate the exception to the calling method.
You're running in two problems:
async void has a slightly different error handling in comparision to traditional void methods: Although StartReceiveMessagesAsync is called by GetAmazonMwsNotifications and it blocks GetAmazonMwsNotifications until the first await (on an uncompleted Task) is reached, any exceptions from within StartReceiveMessagesAsync are never thrown back to GetAmazonMwsNotifications. In non UI applications they are always thrown onto the threadpool, bringing the application down (I don't know how UI applications work in that case).
So why doesn't your application die?
The exception is not thrown onto the stack, it's set onto the Task that is returned by Task.Factory.StartNew and this Task is not observed (neither via await nor via .Wait()). At some point the Garbage Collector will run to collect that Task and at that point an UnobservedTaskException will be thrown on the appdomain. When this is not observed your application will finally go down.
In my opinion you don't need to offload the code via Task.Run/Task.Factory.StartNew, await the result of ReceiveMessageAsync instead of blocking on it and handle exceptions in the async void method the "usual" way; just keep in mind that unhandled exceptions will bring down the application.
You can continue with another task with the OnlyOnFaulted option (i.e. an exception has been thrown and the task is in the faulted state).
.ContinueWith(t => { Console.WriteLine(t.Exception.Message); },
TaskContinuationOptions.OnlyOnFaulted);

NotOnRanToCompletion Continuation doesn't run when parent task is cancelled

I'm trying to test a scenario where I have a task that can be cancelled, and a continuation that should be running if the antecedent task doesn't complete. A sample of the code is like this:
static void Main(string[] args)
{
var source = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
{
while (true)
{
source.Token.ThrowIfCancellationRequested();
}
}, source.Token);
var continuation = task.ContinueWith(t =>
{
Console.WriteLine("Continuation");
if (t.Status == TaskStatus.Faulted)
{
Console.WriteLine("Antecedent Faulted: " + t.Exception.Message);
}
}, source.Token, TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.AttachedToParent, TaskScheduler.Current);
var cancellation = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
source.Cancel();
});
try
{
continuation.Wait();
}
catch (AggregateException)
{
Console.WriteLine("AggregateException");
}
Console.WriteLine("Done");
while (!Console.KeyAvailable) { }
}
The output of this program is:
AggregateException
Done
To a certain extent, I get it. The primary task was cancelled, which ends up throwing a TaskCanceledException which gets wrapped in the AggregateException. The question is, is this expected behaviour? If so, what use is TaskStatus.Faulted in a continuation, if that continuation isn't getting executed? I even set a looping check for ConsoleKeyAvailable just in case the continuation does get run as well as the AggregateException getting thrown.
I believe the reason this is occurring is you are passing source.Token as the CancellationToken for the call to task.ContinueWith. Despite passing NotOnRanToCompletion as the continuation options, the fact that the token is cancelled before the continuation task ever starts means the scheduler can immediately transition it to the canceled state without actually running it.

Task.Wait unexpected behavior in case of OperationCanceledException

Consider the following piece of code:
CancellationTokenSource cts0 = new CancellationTokenSource(), cts1 = new CancellationTokenSource();
try
{
var task = Task.Run(() => { throw new OperationCanceledException("123", cts0.Token); }, cts1.Token);
task.Wait();
}
catch (AggregateException ae) { Console.WriteLine(ae.InnerException); }
Due to MSDN task should be in Faulted state because it's token does not match exception's token (and also IsCancellationRequested is false):
If the token's IsCancellationRequested property returns false or if the exception's token does not match the Task's token, the OperationCanceledException is treated like a normal exception, causing the Task to transition to the Faulted state.
When I launch this code in console app using .NET 4.5.2 I get task in Canceled state (aggregate exception contains unknown TaskCanceledExeption, not the original). And all information of original exception is lost (message, inner exception, custom data).
I also noticed that behavior of Task.Wait differs from await task in case of OperationCanceledException.
try { Task.Run(() => { throw new InvalidOperationException("123"); }).Wait(); } // 1
catch (AggregateException ae) { Console.WriteLine(ae.InnerException); }
try { await Task.Run(() => { throw new InvalidOperationException("123"); }); } // 2
catch (InvalidOperationException ex) { Console.WriteLine(ex); }
try { Task.Run(() => { throw new OperationCanceledException("123"); }).Wait(); } // 3
catch (AggregateException ae) { Console.WriteLine(ae.InnerException); }
try { await Task.Run(() => { throw new OperationCanceledException("123"); }); } // 4
catch (OperationCanceledException ex) { Console.WriteLine(ex); }
Cases 1 and 2 produce almost identical result (differ only in StackTrace), but when I change exception to OperationCanceledException, then I get very different results: an unknown TaskCanceledException in case 3 without original data, and expected OpeartionCanceledException in case 4 with all original data (message, etc.).
So the question is: Does MSDN contain incorrect information? Or is it a bug in .NET? Or maybe it's just I don't understand something?
It is a bug. Task.Run under the hood calls Task<Task>.Factory.StartNew. This internal Task is getting the right Status of Faulted. The wrapping task is not.
You can work around this bug by calling
Task.Factory.StartNew(() => { throw new OperationCanceledException("123", cts0.Token); }, cts1.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
Though, you'll lose the other feature of Task.Run which is unwrapping. See:
Task.Run vs Task.Factory.StartNew
More Details:
Here's the code of Task.Run where you see that it is creating a wrapping UnwrapPromise (which derives from Task<TResult>:
public static Task Run(Func<Task> function, CancellationToken cancellationToken)
{
// Check arguments
if (function == null) throw new ArgumentNullException("function");
Contract.EndContractBlock();
cancellationToken.ThrowIfSourceDisposed();
// Short-circuit if we are given a pre-canceled token
if (cancellationToken.IsCancellationRequested)
return Task.FromCancellation(cancellationToken);
// Kick off initial Task, which will call the user-supplied function and yield a Task.
Task<Task> task1 = Task<Task>.Factory.StartNew(function, cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
// Create a promise-style Task to be used as a proxy for the operation
// Set lookForOce == true so that unwrap logic can be on the lookout for OCEs thrown as faults from task1, to support in-delegate cancellation.
UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1, lookForOce: true);
return promise;
}
The Task constructor which it calls does not take a cancellation token (and thus it does not know about the inner Task's cancellation token). Notice it creates a default CancellationToken instead. Here's the ctor it calls:
internal Task(object state, TaskCreationOptions creationOptions, bool promiseStyle)
{
Contract.Assert(promiseStyle, "Promise CTOR: promiseStyle was false");
// Check the creationOptions. We only allow the AttachedToParent option to be specified for promise tasks.
if ((creationOptions & ~TaskCreationOptions.AttachedToParent) != 0)
{
throw new ArgumentOutOfRangeException("creationOptions");
}
// m_parent is readonly, and so must be set in the constructor.
// Only set a parent if AttachedToParent is specified.
if ((creationOptions & TaskCreationOptions.AttachedToParent) != 0)
m_parent = Task.InternalCurrent;
TaskConstructorCore(null, state, default(CancellationToken), creationOptions, InternalTaskOptions.PromiseTask, null);
}
The outer task (the UnwrapPromise adds a continuation). The continuation examines the inner task. In the case of the inner task being faulted, it consideres finding a a OperationCanceledException as indicating cancellation (regardless of a matching token). Below is the UnwrapPromise<TResult>.TrySetFromTask (below is also the call stack showing where it gets called). Notice the Faulted state:
private bool TrySetFromTask(Task task, bool lookForOce)
{
Contract.Requires(task != null && task.IsCompleted, "TrySetFromTask: Expected task to have completed.");
bool result = false;
switch (task.Status)
{
case TaskStatus.Canceled:
result = TrySetCanceled(task.CancellationToken, task.GetCancellationExceptionDispatchInfo());
break;
case TaskStatus.Faulted:
var edis = task.GetExceptionDispatchInfos();
ExceptionDispatchInfo oceEdi;
OperationCanceledException oce;
if (lookForOce && edis.Count > 0 &&
(oceEdi = edis[0]) != null &&
(oce = oceEdi.SourceException as OperationCanceledException) != null)
{
result = TrySetCanceled(oce.CancellationToken, oceEdi);
}
else
{
result = TrySetException(edis);
}
break;
case TaskStatus.RanToCompletion:
var taskTResult = task as Task<TResult>;
result = TrySetResult(taskTResult != null ? taskTResult.Result : default(TResult));
break;
}
return result;
}
Call stack:
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetCanceled(System.Threading.CancellationToken tokenToRecord, object cancellationException) Line 645 C#
mscorlib.dll!System.Threading.Tasks.UnwrapPromise<System.Threading.Tasks.VoidTaskResult>.TrySetFromTask(System.Threading.Tasks.Task task, bool lookForOce) Line 6988 + 0x9f bytes C#
mscorlib.dll!System.Threading.Tasks.UnwrapPromise<System.Threading.Tasks.VoidTaskResult>.ProcessCompletedOuterTask(System.Threading.Tasks.Task task) Line 6956 + 0xe bytes C#
mscorlib.dll!System.Threading.Tasks.UnwrapPromise<System.Threading.Tasks.VoidTaskResult>.InvokeCore(System.Threading.Tasks.Task completingTask) Line 6910 + 0x7 bytes C#
mscorlib.dll!System.Threading.Tasks.UnwrapPromise<System.Threading.Tasks.VoidTaskResult>.Invoke(System.Threading.Tasks.Task completingTask) Line 6891 + 0x9 bytes C#
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations() Line 3571 C#
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Line 2323 + 0x7 bytes C#
mscorlib.dll!System.Threading.Tasks.Task.FinishStageTwo() Line 2294 + 0x7 bytes C#
mscorlib.dll!System.Threading.Tasks.Task.Finish(bool bUserDelegateExecuted) Line 2233 C#
mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) Line 2785 + 0xc bytes C#
mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Line 2728 C#
mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Line 2664 + 0x7 bytes C#
mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() Line 829 C#
mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Line 1170 + 0x5 bytes C#
It notices the OperationCanceledException and calls TrySetCanceled to put the task into the cancelled state.
An aside:
Another thing to note is that when you start using async methods, there isn't really a way to register a cancellation token with an async method. Thus, any OperationCancelledException that gets encountered in an async methods is considered a cancellation.
See
Associate a CancellationToken with an async method's Task
Matt Smith - Thank you, your explanation was very helpful.
After reading it and testing for a while I noticed that original question does not fully correct. It's not a problem of Task.Wait. I can get this wrong behavior with Task.ContinueWith, checking first task's Status - it is Canceled. So I believe the final answer is:
If you create a task using Task.Run overloads that take Func<Task> or Func<Task<TResult>> as a first argument, and your delegate throws OperationCanceledException, and if you use Task.Wait or Task.ContinueWith on returned task, then you will lose original exception with all it's data because of bug in .NET (as Matt Smith explained) and get task in incorrect Canceled state instead of Faulted, regardless of matching documented logic.
All of these conditions matter. If you use await on created task - it works fine. If you use Task.Run overloads that take Action or Func<TResult> as a first argument - it works fine in all cases (Wait, ContinueWith, await).
I also noticed strange behavior of overloaded method selection logic. When I write
Task.Run(() => { throw new OperationCanceledException("123", cts0.Token); }, cts1.Token);
I expect to use Task.Run(Action, CancellationToken) overload, which is not broken. But somehow it appears that broken Task.Run(Func<Task>, CancellationToken) is used. So I'm forced to do something like this
Task.Run((Action)(() => { throw new OperationCanceledException("123", cts0.Token); }), cts1.Token);
or use TaskFactory.StartNew.
This behavior is very interesting and strange, at the same time.
The purpose of the AggregateException, as its name says, is to group together multiple exceptions/errors that happen during the execution of an application. So, in your 3rd case, you have an OperationCanceledException as inner exception and the stack trace of the AggregateException should report everything about that, including the data (like 123), as it is shown below in the 4th case:
About your questions:
Does MSDN contain incorrect information?
It should report always the correct and precise information about the behavior of classes, methods and so on.
Or is it a bug in .NET?
Most probably, yes, it is a bug. It's not understandable why this happens. Here you'll find a related question about this issue. Please report this problem to Microsoft.

Categories

Resources