3

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.

Duleb
  • 571
  • 1
  • 5
  • 12
  • 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 Feb 11 '17 at 20:41
  • @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 Feb 11 '17 at 20:52
  • @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 Feb 11 '17 at 21:36
  • @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 Feb 11 '17 at 21:41
  • 1
    @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 Feb 11 '17 at 22:18
  • @sellotape: Thank you for your response. ContinueWith still has it's place in the new world. Consider this scenario. My code start, launches some side task to do some critical polling and then continues to execute some other important operations (which can happen in parallel to side task). Now I can't await the code that launched the polling task but I also want to ensure that If that task runs into a problem, I crash my application. In this case I use continue with. Basically when I launch the poll task, I specify in my continue with that if this task gets into faulted state, Take Action X. – Duleb Feb 12 '17 at 06:58
  • And Yes, as suggested in some of the comments, one of the fix is to await the invocation of RunTestTaskAsync (like var createdTask = Task.Run(async () => await this.RunTestTaskAsync(CancellationToken.None).ConfigureAwait(false), CancellationToken.None); ). Other fix is listed in the Update to the question. – Duleb Feb 12 '17 at 07:00
  • Yes Peter you are right and I have marked your response as answer. Colleague pointed out it in email which I track more actively and in my defense, he explained it more succinctly :) Again, absolutely no intention to be rude and only Thanks to you and the rest of the community. – Duleb Feb 12 '17 at 20:20

1 Answers1

-1

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.)

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • What's the point of configuring `await` (i.e. `createdTask.ConfigureAwait(false)`) without a corresponding `await`? And looking more closely, it seems like that code never runs (an exception will be thrown before then). – ta.speot.is Feb 12 '17 at 07:00
  • 1
    @ta.speot.is: the continuation is affected by a call to `ConfigureAwait()`, whether done via `ContinueWith()` or an `await` statement. And you are also mistaken about the exception. It's not observed in the thread, because it's encapsulated by the `Task` returned by the method. If you'd bothered to run the code, you'd know that. Funny how people down-vote without really understanding what they are looking at. – Peter Duniho Feb 12 '17 at 08:13
  • RE: #1 I'm under the impression `ConfigureAwait` returns a `ConfiguredTaskAwaitable` and *that's* the thing that holds the configuration. You're right RE: #2. – ta.speot.is Feb 12 '17 at 08:30
  • 1
    @ta.speot.is: yes, it's correct. You'd have to pass a scheduler to ContinueWith(). The awaitable is discarded and has no effect. Still, the point is that the answer is correct, and the code is written simply to match the original question's code as closely as possible while providing a behavior that matches the OP's expectations. Hardly worth down-voting, just because the original code was imperfect. – Peter Duniho Feb 12 '17 at 08:59