3
votes

So here is what I'm trying to achieve. I launch a task and don't do wait/result on it. To ensure that if launched task goes to faulted state (for e.g. say throw an exception) I crash the process by calling Environment FailFast in Continuation.

How the problem I'm facing is that If I ran below code, Inside ContinueWith, the status of the task (which threw exception) shows up as "RanToCompletion". I expected it to be Faulted State.

    private Task KickOfTaskWorkAsync()
    {
        var createdTask = Task.Run(() => this.RunTestTaskAsync(CancellationToken.None).ConfigureAwait(false), CancellationToken.None);

        createdTask.ContinueWith(
            task => Console.WriteLine("Task State In Continue with => {0}", task.Status));

        return createdTask;
    }

    private async Task RunTestTaskAsync(CancellationToken cancellationToken)
    {
        throw new Exception("CrashingRoutine: Crashing by Design");
    }

This is really strange :( If I remove the 'ConfigureAwait(false)' inside Task.Run function call, the task does goes to Faulted state inside Continue with. Really at loss to explain what's going on and would appreciate some help from community.

[Update]: My colleague pointed out an obvious error. I am using ConfigureAwait while I make a call to RunTestAsync inside Test.Run even though I don't await it. In this case, ConfigureAwait doesn't return a Task to Task.Run. If I don't call ConfigureAwait, a Task does get returned and things work as expected.

1
Is there a reason you're using ContinueWith? ContinueWith was for the pre-async-await era; now you can just use await RunTestTaskAsync() followed by Console.WriteLine() and catch any exceptions you want in the more standard way; i.e. try..catch.sellotape
@sellotape: it is correct that await is a more idiomatic way to deal with continuations now. However, the OP would still have the same issue even using await. They would be awaiting the wrong Task object and would still see the task in a non-faulted state.Peter Duniho
@PeterDuniho - if he were to await createdTask, then yes, but if he just awaits RunTestTaskAsync() (which there seems little reason not to, but I guess there's not enough context to be sure) the catch will catch the exception he throws.sellotape
@sellotape: "if he just awaits RunTestTaskAsync()" -- but he's not. And that he's not is the entire crux of his problem. My point is that, just telling him to use await doesn't address his question at all. If ContinueWith() is replaced with await here, the same issue will still happen. The question would just have slightly reworded code.Peter Duniho
@PeterDuniho - I wasn't suggesting it's an answer; rather just relevant information the OP might find useful, which is why I posted a comment, not an answer (your answer covers the question anyway). I also did specifically say await RunTestTaskAsync() in the first comment.sellotape

1 Answers

-1
votes

Your error is a specific example of a broader category of mistake: you are not observing the Task you actually care about.

In your code example, the RunTestTaskAsync() returns a Task object. It completes synchronously (because there's no await), so the Task object it returns is already faulted when the method returns, due to the exception. Your code then calls ConfigureAwait() on this faulted Task object.

But all of this happens inside another Task, i.e. the one that you start when you call Task.Run(). This Task doesn't do anything to observe the exception, so it completes normally.

The reason you observe the exception when you remove the ConfigureAwait() call has nothing to do with the call itself. If you left the call and passed true instead, you would still fail to observe the exception. The reason you can observe the exception when you remove the call is that, without the call to ConfigureAwait(), the return value of the lambda expression is a Task, and this calls a different overload of Task.Run().

This overload is a bit different from the others. From the documentation:

Queues the specified work to run on the thread pool and returns a proxy for the task returned by function.

That is, while it still starts a new Task, the Task object it returns represents not that Task, but the one returned by your lambda expression. And that proxy takes on the same state as the Task it wraps, so you see it in the Faulted state.

Based on the code you posted, I would say that you shouldn't be calling Task.Run() in the first place. The following will work just as well, without the overhead and complication of the proxy:

static void Main(string[] args)
{
    Task createdTask = RunTestTaskAsync();

    createdTask.ConfigureAwait(false);

    createdTask.ContinueWith(
        task => Console.WriteLine("Task State In Continue with => {0}", task.Status)).Wait();
}

private static async Task RunTestTaskAsync()
{
    throw new Exception("CrashingRoutine: Crashing by Design");
}

(I removed the CancellationToken values, because they have nothing at all to do with your question and are completely superfluous here.)