Diego Mijelshon
Diego Mijelshon

Reputation: 52725

Catching exceptions in tasks

My base ViewModel, implemented a couple years ago, provides this extension method for running tasks while keeping the UI responsive*:

protected void Work(Action job)
{
    IsBusy = true;
    var stackTrace = new StackTrace();
    var task = Task.Factory.StartNew(job)
        .ContinueWith(failedTask => HandleException(failedTask, stackTrace), 
                      TaskContinuationOptions.OnlyOnFaulted)
        .ContinueWith(_ => { IsBusy = false; });
}

void HandleException(Task task, StackTrace stackTrace)
{
    Dispatcher.BeginInvoke(
        () => { throw new Exception(task.Exception.InnerException.ToString() + 
                                    stackTrace); });
}

IsBusy is a property observed from the UI to display a progress bar.

The idea behind HandleExceptions is that they are observed and then thrown in the UI thread, caught by a try/catch block in the Main() method and logged before displaying a friendly message to the user and safely closing the app. The stackTrace is passed so the log includes the caller information.

However, I recently started getting reports of the application crashing without logging, nor friendly messages, with this Windows dialog:

crash

Looking at the Windows event log, we got this EventData:

Application: xxxxApplication.Loader.exe

Framework Version: v4.0.30319

Description: The process was terminated due to an unhandled exception.

Exception Info: System.AggregateException

Stack: at System.Threading.Tasks.TaskExceptionHolder.Finalize()

Am I doing anything wrong with ContinueWith()?

Is it possible for task exceptions to somehow stay unobserved?


*: I know about BackgroundWorker. This probably seemed like a better idea at the time, or had additional benefits.

Upvotes: 4

Views: 3535

Answers (3)

gwiazdorrr
gwiazdorrr

Reputation: 6329

My guess is that you've got a race condition here. From Task.Exception reference:

Tasks that throw unhandled exceptions store the resulting exception and propagate it wrapped in a AggregateException in calls to Wait or in accesses to the Exception property. Any exceptions not observed by the time the task instance is garbage collected will be propagated on the finalizer thread. For more information and an example, see Exception Handling (Task Parallel Library).

Bottomline, if your task gets collected before the delegate passed in Dispatcher.BeginInvoke gets invoked the exception will be thrown in the main thread. A quick fix:

void HandleException(Task task, StackTrace stackTrace)
{
    // "observe" the exception
    var ex = task.Exception;
    Dispatcher.BeginInvoke(
        () => { throw new Exception(ex.InnerException.ToString() + 
                                    stackTrace); });
}

Upvotes: 1

Francois Nel
Francois Nel

Reputation: 1812

I would suggest you handle the AppDomain.CurrentDomain.UnhandledException to either display your "friendly message" or (if this is not a solution for you) you could at least log the errors within this event and then see what the stacktrace is to determine where this "un-handled" task exception occurs.

Upvotes: 1

flq
flq

Reputation: 22849

I am not too sure, but you would obtain the original exception through the failedTask variable. It provides a non-null AggregateException if the Task failed.

That Exception has in turn a Handle method which you can provide a delegate where you can handle each Exception contained in the AggregateException and state whether you consider the exception handled or not.

In other words, if you want to rethrow the actual exception on the Dispatcher thread, I think you should make sure that you marked the original exception as handled before leaving the Task chain.

Upvotes: 0

Related Questions