From the comment:
I can see that there is a reason for everything, but I can't deny being flabbergasted by the idea that there's a good reason that this should not happen by design. What if I don't want to await a Task, should I just never use the Task class in that case? What's the drawback to this having an event that can be subscribed to? I'm going to write an extension method, and by all means, tell me why it shouldn't be this way
The nature of the Task
object is that it is something that gets completed in the future. The result or exception of the task is also expected to get observed in the future, quite likely on a different stack frame or even on a different thread. That's why the Framework (or compiler-generated code, for async Task
methods) stores the exception inside the task and doesn't throw it immediately.
You do not observe the result of the task here and you do not even store a reference to the task object anywhere. Essentially, you're doing a fire-and-forget call. The compiler warns about that (telling that the task is not awaited), but it doesn't eliminate the fact the the managed Task
object still gets created and is hanging around until it gets garbage-collected. At that point, TaskScheduler.UnobservedTaskException
event will be fired.
... there's a good reason that should not happen by design
Now if the above approach is a bad design, what would be a good one? If the exception would be thrown immediately, how could the following possibly work:
var task = Task.Run(() => {
throw new ApplicationException("I'll throw an unhanded exception"); });
Thread.Sleep(1000);
if (task.IsFaulted)
Console.WriteLine(task.Exception.InnerException.Message);
It simply could not. The code after Sleep
would not have a chance to handle the exception its way. Thus, the current behavior is well thought out and makes perfect sense.
If you still want to observe exception as soon as it happens for a fire-and-forget task, use a helper async void
method:
public static class TaskExt
{
public static async void Observe(
this Task @this,
bool continueOnCapturedContext = true)
{
await @this.ConfigureAwait(continueOnCapturedContext);
}
}
static void Main(string[] args)
{
TaskScheduler.UnobservedTaskException += Handler;
AppDomain.CurrentDomain.UnhandledException += Handler;
Task.Run(() => { throw new ApplicationException("I'll throw an unhanded exception"); })
.Observe();
Task.Factory.StartNew(() => { throw new ApplicationException("I'll throw an unhanded exception too"); })
.Observe();
System.Threading.Thread.Sleep(2000);
Console.WriteLine("I think everything is just peachy!");
System.Threading.Thread.Sleep(10000);
}
You can also use an async void
lambda for a fire-and-forget call:
Action fireAndForget = async () =>
{
try
{
await Task.Run(...);
}
catch(e) { /* handle errors */ };
};
fireAndForget();
I described the exception propagation behavior for async void
methods in more details here.
OK, I get what you're saying. By design, are tasks meant to be never used with a fire and forget pattern? Just dispatch the work to an anonymous thread some other way?
I'm not saying that tasks aren't meant to be used with fire-and-forget pattern. I actually think they're perfectly suited for that. There are some options out there to implement this pattern with tasks, I showed one above, another one is Task.ContinueWith
, or you can check this question for some more ideas.
A different matter is that I hardly can imagine a useful scenario where I would not want to observe the completion status of the task, even for a fire-and-forget call like above. What job would such a task do? Even for logging, I'd still want to make sure a log enty was successfully written. Besides, what if the parent process ends before the task is complete? In my experience, I've always used something like QueueAsync
to keep tracks of the fired tasks.