NetCore Tutorials - Wiring Up GlobalException Handlers

This guide demonstrates a way to incorporate the two most common global exception handlers needed.

  1. Create a Program.cs that will handle the two main global exceptions that can occur.

Getting Started

NuGets

None

EventHandlers wired up to Events

using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace NetCore.GlobalExceptionHandler
{
    public static class Program
    {
        private static string DefaultErrorMessage { get; set; } = "Unhandled Error Occurred. No details are known.";

        public static void Main(string[] args)
        {
            // Exception Handling Wiring
            AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; // Adding to the Handlers, not assigning (there maybe others)
            TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler; // Adding to the Handlers, not assigning

            Console.ReadLine();
        }

        #region Helpers

        private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {

        }

        private static void UnobservedTaskExceptionHandler(object sender, UnobservedTaskExceptionEventArgs e)
        {

        }

        #endregion
    }
}

Three key things to take away from this are as follows.

First, know the meaning of an EventHandler being invoked when subscribed to an Event that fires. Obligatory Microsoft Link

Second, AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;, is stating that for this AppDomain.CurrentDomain (this application's domain space), we want an Event called UnhandledException being additionally handled by UnhandledExceptionHandler event handler so that will execute when the event is fired/invoked.

Third, TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler;, is stating that when TaskScheduler has an UnobservedException event, it also fires/invokes the UnobservedTaskExceptionHandler event handler.

Lastly, why do we do this? Mainly, to prevent a premature or unexpected shutdown of our application, i.e. an application crash from occurring. That isn't the only reason, for example we may want to log errors to the EventViewer (here is a guide for that.)

UnhandledExceptionHandler

private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
{
    try
    {
        var message = DefaultErrorMessage;

        if (e.ExceptionObject != null && e.ExceptionObject is Exception uex)
        {
            message = string.Format("{0} Exception: {1}", DefaultErrorMessage, uex.Message);
        }
        else if (sender is Exception ex)
        {
            message = string.Format("{0} Exception: {1}", DefaultErrorMessage, ex.Message);
        }

        Trace.Write(message);
    }
    catch { } // Swallow exception
}

You want to first look for the real exception in EventArgs passed into our handler. Because of the complex nature of exceptions, this data isn't always populated. Whatever the occassions, for example cross threading access violation or unmanaged heap violations, they can't always get assigned cleanly into the EventArgs. That's why we look at the sender object as a fallback.

These events fire when exceptions simply occurred outside of any Try {} Catch {} and were thrown.

UnobservedTaskExceptionHandler

private static void UnobservedTaskExceptionHandler(object sender, UnobservedTaskExceptionEventArgs e)
{
    try
    {
        var message = DefaultErrorMessage;
        e?.SetObserved(); // Prevents the Program from terminating.

        if (e.Exception != null && e.Exception is Exception tuex)
        {
            message = string.Format("{0} Exception: {1}", DefaultErrorMessage, tuex.Message);
        }
        else if (sender is Exception ex)
        {
            message = string.Format("{0} Exception: {1}", DefaultErrorMessage, ex.Message);
        }

        Trace.Write(message);
    }
    catch { } // Swallow exception
}

Here we have the UnobservedTaskExceptionHandler, who fires only when the GC detects the Task is InFaulted status and no one osberved the fault/error. This can be a huge pain trying to resolve if you have a lot of fire and forget Tasks, or worse, a library is triggering this through bad code... TaskCompletionSource anyone?

The most important part of this code is this: e?.SetObserved();. Without this, your application will always terminate or shutdown.

Everything Together In Program.cs

using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace NetCore.GlobalExceptionHandler
{
    public static class Program
    {
        private static string DefaultErrorMessage { get; set; } = "Unhandled Error Occurred. No details are known.";
        private const string SourceName = "ApplicationName";
        private const string LogName = "Application";

        public static void Main(string[] args)
        {
            // Exception Handling Wiring
            AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;
            TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler;

            Console.ReadLine();
        }

        #region Helpers

        private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            try
            {
                var message = DefaultErrorMessage;

                if (e.ExceptionObject != null && e.ExceptionObject is Exception uex)
                {
                    message = string.Format("{0} Exception: {1}", DefaultErrorMessage, uex.Message);
                }
                else if (sender is Exception ex)
                {
                    message = string.Format("{0} Exception: {1}", DefaultErrorMessage, ex.Message);
                }

                Trace.WriteLine(message);
            }
            catch { } // Swallow exception
        }

        private static void UnobservedTaskExceptionHandler(object sender, UnobservedTaskExceptionEventArgs e)
        {
            try
            {
                var message = DefaultErrorMessage;
                e?.SetObserved(); // Prevents the Program from terminating.

                if (e.Exception != null && e.Exception is Exception tuex)
                {
                    message = string.Format("{0} Exception: {1}", DefaultErrorMessage, tuex.Message);
                }
                else if (sender is Exception ex)
                {
                    message = string.Format("{0} Exception: {1}", DefaultErrorMessage, ex.Message);
                }

                Trace.WriteLine(message);
            }
            catch { } // Swallow exception
        }

        #endregion
    }
}

We added some Trace (vs. Console) for good measure since more than likely, this is going to a remote machine or cloud as a service and not run locally in a console. You will want to see those outputs then and Trace is the way to do it.

There you have it, a bit of a saftey net to prevent your application from crashing! Althought a wonderful goal... app crash prevention, the real adult reason to use these are for logging. The logs are there to better identify the unhandled errors when chaos happens - have at least a StackTrace to start some RCA.

Check out the next guide to also incorporate EventViewer with these Global Exception Handlers and a little code cleanup.