In TPL, if an exception is thrown by a Task, that exception is captured and stored in Task.Exception, and then follows all the rules on observed exceptions. If it's never observed, it's eventually rethrown on the finalizer thread and crashes the process.
Is there a way to prevent the Task from catching that exception, and just letting it propagate instead?
The Task I'm interested in would already be running on the UI thread (courtesy of TaskScheduler.FromCurrentSynchronizationContext), and I want the exception to escape so it can be handled by my existing Application.ThreadException handler.
I basically want unhandled exceptions in the Task to behave like unhandled exceptions in a button-click handler: immediately propagate on the UI thread, and be handled by ThreadException.
I found a solution that works adequately some of the time.
Single task
This schedules a call to
task.Wait()
on the UI thread. Since I don't do theWait
until I know the task is already done, it won't actually block; it will just check to see if there was an exception, and if so, it will throw. Since theSynchronizationContext.Post
callback is executed straight from the message loop (outside the context of aTask
), the TPL won't stop the exception, and it can propagate normally -- just as if it was an unhandled exception in a button-click handler.One extra wrinkle is that I don't want to call
WaitAll
if the task was canceled. If you wait on a canceled task, TPL throws aTaskCanceledException
, which it makes no sense to re-throw.Multiple tasks
In my actual code, I have multiple tasks -- an initial task and multiple continuations. If any of those (potentially more than one) get an exception, I want to propagate an
AggregateException
back to the UI thread. Here's how to handle that:Same story: once all the tasks have completed, call
WaitAll
outside the context of aTask
. It won't block, since the tasks are already completed; it's just an easy way to throw anAggregateException
if any of the tasks faulted.At first I worried that, if one of the continuation tasks used something like
TaskContinuationOptions.OnlyOnRanToCompletion
, and the first task faulted, then theWaitAll
call might hang (since the continuation task would never run, and I worried thatWaitAll
would block waiting for it to run). But it turns out the TPL designers were cleverer than that -- if the continuation task won't be run because ofOnlyOn
orNotOn
flags, that continuation task transitions to theCanceled
state, so it won't block theWaitAll
.Edit
When I use the multiple-tasks version, the
WaitAll
call throws anAggregateException
, but thatAggregateException
doesn't make it through to theThreadException
handler: instead only one of its inner exceptions gets passed toThreadException
. So if multiple tasks threw exceptions, only one of them reaches the thread-exception handler. I'm not clear on why this is, but I'm trying to figure it out.There's no way that I'm aware of to have these exceptions propagate up like exceptions from the main thread. Why not just hook the same handler that you're hooking to
Application.ThreadException
toTaskScheduler.UnobservedTaskException
as well?Does something like this suit?
Ok Joe... as promised, here's how you can generically solve this problem with a custom
TaskScheduler
subclass. I've tested this implementation and it works like a charm. Don't forget you can't have the debugger attached if you want to seeApplication.ThreadException
to actually fire!!!The Custom TaskScheduler
This custom TaskScheduler implementation gets tied to a specific
SynchronizationContext
at "birth" and will take each incomingTask
that it needs to execute, chain a Continuation on to it that will only fire if the logicalTask
faults and, when that fires, itPost
s back to the SynchronizationContext where it will throw the exception from theTask
that faulted.Some notes/disclaimers on this implementation:
Task
to be worked on. I leave this as an excercise for the reader. It's not hard, just... not necessary to demonstrate the functionality you're asking for.Ok, now you have a couple options for using this TaskScheduler:
Pre-configure TaskFactory Instance
This approach allows you to setup a
TaskFactory
once and then any task you start with that factory instance will use the customTaskScheduler
. That would basically look something like this:At application startup
Throughout code
Explicit TaskScheduler Per-Call
Another approach is to just create an instance of the custom
TaskScheduler
and then pass that intoStartNew
on the defaultTaskFactory
every time you start a task.At application startup
Throughout code