13

I encountered an issue when I was using Task.Factory.StartNew and tried to capture an exception that is thrown. In my application I have a long running task that I want to encapsulate in a Task.Factory.StartNew(.., TaskCreationOptions.LongRunning);

However, the exception isn't caught when I'm using Task.Factory.StartNew. It is however working as I expect when I use Task.Run, which I thought was just a wrapper on Task.Factory.StartNew (according to for instance this MSDN article).

A working example is provided here, the difference being that the exception is written to console when using Task.Run, but not when using Factory.StartNew.

My question would be:
if I have a LongRunning task that has the possibility to throw exceptions, how should I handle them in the calling code?

private static void Main(string[] args)
{
    Task<bool> t = RunLongTask();
    t.Wait();
    Console.WriteLine(t.Result);
    Console.ReadKey();
}

private async static Task<bool> RunLongTask()
{
    try
    {
        await RunTaskAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        return false;
    }
    Console.WriteLine("success");
    return true;
}

private static Task RunTaskAsync()
{
    //return Task.Run(async () =>
    //    {
    //        throw new Exception("my exception");
    //    });
    return Task.Factory.StartNew(
        async () =>
    {
        throw new Exception("my exception");
    });

}
default
  • 11,485
  • 9
  • 66
  • 102
  • What happens if you do Task.Factory.StartNew(someAction, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default) - do you get the same result? – Matthew Watson Feb 06 '13 at 13:06
  • @MatthewWatson yes, same result unfortunately – default Feb 06 '13 at 13:07
  • Also, see here: http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx (one of the comments near the end asks a similar question) – Matthew Watson Feb 06 '13 at 13:13
  • @MatthewWatson I can't see which comment though.. The one from `nick` seems to only handle the difference between `await Task.Run(() => ..` and `await Task.Run(async () => ..` – default Feb 06 '13 at 13:17
  • Hmm yes, I was assuming the async thing was something to do with it. Perhaps it's not! – Matthew Watson Feb 06 '13 at 13:35

3 Answers3

16

Your problem is that StartNew doesn't work like Task.Run with async delegates. The return type of StartNew is Task<Task> (which is convertible to Task). The "outer" Task represents the beginning of the method, and the "inner" Task represents the completion of the method (including any exceptions).

To get to the inner Task, you can use Unwrap. Or you can just use Task.Run instead of StartNew for async code. LongRunning is just an optimization hint and is really optional. Stephen Toub has a good blog post on the difference between StartNew and Run and why Run is (usually) better for async code.

Update from @usr comment below: LongRunning only applies to the beginning of the async method (up until the first incomplete operation is awaited). So it's almost certainly better all around to use Task.Run in this case.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • At my work, we are however encouraged to use these optimization hints. Would it be better if I instead of converting it to a `async void` use a return value from the `Factory.StartNew` (is that even possible)? – default Feb 06 '13 at 13:13
  • 1
    To reduce confusion, I'd recommend making it a separate method. Also read Stephen Toub's blog post; if you use the older `StartNew` with the newer `async` code, you probably want to also call `Unwrap`. – Stephen Cleary Feb 06 '13 at 13:18
  • My answer was incorrect; it wasn't a lambda conversion causing the issue. It was the wrapping of the `Task`. I've updated my answer to be... well... *right*. :) – Stephen Cleary Feb 06 '13 at 13:35
  • @StephenCleary This is confusing, according to ReSharper, your previous answer was correct. But according to VS, your new answer is. – svick Feb 06 '13 at 13:37
  • I encountered a new strange thing. `Unwrap` does solve the issue so that's great! However, the intellisense is complaining that the return value of `(async () => { .. }, TaskCreationOptions.LongRunning).Unwrap();` *cannot convert instance argument type 'Task' to 'Task'*. It compiles and runs fine though – default Feb 06 '13 at 13:38
  • @Default Are you using ReSharper? – svick Feb 06 '13 at 13:39
  • 1
    LongRunning is identical to forcing a new thread to be created in practice. And your async method is probably not on that thread for a long time (it is taken off at the first await point). You don't *want* LongRunning in this case. – usr Feb 06 '13 at 13:40
  • @svick - yes I am. Is it Re# that is complaining? It looked like an intellisense error – default Feb 06 '13 at 13:40
  • @Default Yeah, I think it is. I believe ReSharper overrides IntelliSense. – svick Feb 06 '13 at 13:41
  • @usr this is only example code. The code we are working with does imply a long running operation, thus why we are using it. – default Feb 06 '13 at 13:41
  • @Default so your code will run for a long time until the *first* await point is hit? It does not matter how long the async method runs. The thread is destroyed at the very first await (that operates on a non-completed task). – usr Feb 06 '13 at 13:44
  • @usr It do have the potential to. I might need to read more about this. Is `LongRunning` a suggestion only for the compiler? When running my application, will my Tasks be analyzed and be created in a new thread if needed? Could it be that I am preoptimizing this..? – default Feb 06 '13 at 13:47
  • @svick: I'd tend to believe VS. I've actually gotten out of the habit of using Re# since they didn't adopt `async` as quickly as my code did. :) I don't have the knowledge to say *for sure* which one is right; I've asked Eric Lippert to do a blog post on the updated `async` method resolution rules. But I suppose I could stop being lazy and check out the updated language spec myself. :) – Stephen Cleary Feb 06 '13 at 13:52
  • @usr: Excellent point about losing the `LongRunning` thread! I'll edit that into my answer. – Stephen Cleary Feb 06 '13 at 13:53
  • 1
    @Default the compiler is generally unable to analyze your code in any major way. Also the compiler does not know anything about the TPL. The TPL is a library. And this library will just always launch a new thread. Specify LongRunning iff your task will almost always burn 100% CPU for multiple seconds or will block for multiple seconds with very high probability. – usr Feb 06 '13 at 13:55
  • @usr thank you for your input. Would you mind adding your input as an "assistant answer" so that I can at least upvote it? I think it has high value in this thread. – default Feb 06 '13 at 13:57
2

I'll pull some of my comments into an answer because they turned out to be helpful:

LongRunning is identical to forcing a new thread to be created in practice. And your async method is probably not on that thread for a long time (it is taken off at the first await point). You don't want LongRunning in this case.

It does not matter how long the async method runs. The thread is destroyed at the very first await (that operates on a non-completed task).

Can the compiler use this hint in any way? The compiler is generally unable to analyze your code in any major way. Also the compiler does not know anything about the TPL. The TPL is a library. And this library will just always launch a new thread. Specify LongRunning iff your task will almost always burn 100% CPU for multiple seconds or will block for multiple seconds with very high probability.

My guess is you don't want LongRunning here because if you're blocking, why are you using async in the first place? async is about not blocking but getting off the thread.

usr
  • 168,620
  • 35
  • 240
  • 369
  • do you have any reference about the `LongRunning` being taken of at the first await point? I'd like to read more about it – default Feb 07 '13 at 07:46
  • @Default await queues a callback to be executed when the operation is complete. Having done that it always returns from the function. The TPL sees the return and ends the task (and the thread) (remember, there are two tasks here - see Stephen Cleary's answer. One (outer) task completes at the first await point and returns the inner task while the inner task completes at the end). See the blog series by Eric Lippert called "Asynchrony in C# 5". The language does not guarantee "being taken of at the first await point". This emerges from other behaviors kind of by coincidence. – usr Feb 07 '13 at 09:55
0

It should be possible when you first Unwrap the task:

await RunTaskAsync().Unwrap();

Or alternatively:

await await RunTaskAsync();
Johannes Egger
  • 3,874
  • 27
  • 36