Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Catch exceptions on another thread?

When developing winform applications it is common that you will have to just invoke to get the main GUI thread to do the GUI work.

Invoke are today obsolete(If I read correct), instead you are suppose to use the SynchronizationContext instead.

The question is : How do I handle exceptions? I have notice that sometimes the exception thrown on the "synced/invoked" thread is lost?

I do use the Application.ThreadException and AppDomain.CurrentDomain.UnhandledException but this does not help?

like image 983
Banshee Avatar asked Sep 02 '25 17:09

Banshee


1 Answers

First of all, Synchronization Context is nothing new, it's been there since .NET 2.0. It has nothing to do specifically with exception handling. Neither does it make Control.Invoke obsolete. In fact, WinFormsSynchronizationContext, which is the WinForms' implementation of synchronization context, uses Control.BeginInvoke for Post and Control.Invoke for Send methods.

How do I handle exceptions? I have notice that sometimes the exception thrown on the "synced/invoked" thread is lost?

There is a well-documented behavior behind "sometimes" here. Control.Invoke is a synchronous call, which propagates exceptions from inside the callback to the calling thread:

int Test()
{
    throw new InvalidOperationException("Surpise from the UI thread!");
}

void Form_Load(object sender, EventArgs e)
{
    // UI thread
    ThreadPool.QueueUserWorkItem(x =>
    {
        // pool thread
        try
        {
            this.Invoke((MethodInvoker)Test);
        }
        catch (Exception ex)
        {
            Debug.Print(ex.Message);
        }
    });
}

The benefit of using SynchronizationContext is in de-coupling the WinForms specifics. It makes sense for a portable library, which potentially may be used by WinForms, WPF, Windows Phone, Xamarin or any other client:

// UI thread
var uiSynchronizationContext = System.Threading.SynchronizationContext.Current;
if (uiSynchronizationContext == null)
    throw new NullReferenceException("SynchronizationContext.Current");

ThreadPool.QueueUserWorkItem(x =>
{
    // pool thread
    try
    {
        uiSynchronizationContext.Send(s => Test(), null);
    }
    catch (Exception ex)
    {
        Debug.Print(ex.ToString());
    }
});

Thus, with Control.Invoke (or SynchronizationContext.Send) you have an option to handle exceptions on the calling thread. You don't have such option with Control.BeginInvoke (or SynchronizationContext.Post), by design and by common sense. That's because Control.BeginInvoke is asynchronous, it queues a callback to be executed upon a future iteration of the message loop run by Application.Run.

To be able to handle exception thrown by an asynchronous callback, you'd need to actually observe the completion of the asynchronous operation. Before C# 5.0, you could have used events or Task.ContinueWith for that.

Using an event:

class ErrorEventArgs : EventArgs
{
    public Exception Exception { get; set; }
}

event EventHandler<ErrorEventArgs> Error = delegate { };

void Form_Load(object sender, EventArgs e)
{
    this.Error += (sError, eError) =>
        // handle the error on the UI thread
        Debug.Print(eError.Exception.ToString()); 

    ThreadPool.QueueUserWorkItem(x =>
    {
        this.BeginInvoke(new MethodInvoker(() => 
        {
            try
            {
                Test();
            }
            catch (Exception ex)
            {
                // fire the Error event
                this.Error(this, new ErrorEventArgs { Exception = ex });
            }
        }));
    });
}

Using ContinueWith:

ThreadPool.QueueUserWorkItem(x =>
{
    var tcs = new TaskCompletionSource<int>();

    uiSynchronizationContext.Post(s => 
    {
        try
        {
            tcs.SetResult(Test());
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }
    }, null);

    // observe the completion,
    // only if there's an error
    tcs.Task.ContinueWith(task =>
    {
        // handle the error on a pool thread
        Debug.Print(task.Exception.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);

});

Finally, with C# 5.0, you can use async/await and handle exceptions thrown asynchronously with the same convenience of try/catch you have for synchronous calls:

int Test()
{
    throw new InvalidOperationException("Surpise from the UI thread!");
}

async void Form_Load(object sender, EventArgs e)
{
    // UI thread
    var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    await Task.Run(async () =>
    {
        // pool thread
        try
        {
            await Task.Factory.StartNew(
                () => Test(), 
                CancellationToken.None,
                TaskCreationOptions.None,
                uiTaskScheduler);
        }
        catch (Exception ex)
        {
            // handle the error on a pool thread
            Debug.Print(ex.ToString());
        }
    });
}
like image 101
noseratio Avatar answered Sep 05 '25 05:09

noseratio