Home > Uncategorized > Retrieving state in a FirstChanceException in .NET

Retrieving state in a FirstChanceException in .NET

If you’re using a application-wide FirstChanceException handler, this is great for covering lots of bases at once, it helps make sure that you can catch (and record) an exception even if it happens with a bit of code you didn’t expect.

However, the scope of the handler is what it is, so accessing state, and hence, the context of the error, is going to be tricky. After all, it’s helpful to know where the error happened, but what was the state of your application at the time, is there an edge-case that causes the error?

So, Lets try and demonstrate this issue, in a simplistic example.

static void Main(string[] args)
{
    AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
    {
	Console.WriteLine("An Error occurred, but at what state?");
    };
    Parallel.For(-100, 100, (state) =>
	{
	    if (state == 42)
	    {   
		throw new ArgumentException("Some error with state #42");
	    }
	}
    );
    Thread.Sleep(TimeSpan.FromSeconds(5));
}

So, in this example, we can see that state number 42 causes an error. The FirstChance exception is triggered, but within the handler, we cannot tell what state causes the error.

If we try a naïve approach, and just try storing the state in a static variable, and then report the last known value, let’s see what happens;

private static int lastKnownState = 0;
static void Main(string[] args)
{
    AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
    {
	Console.WriteLine($"Last Known state {lastKnownState}");
	Console.WriteLine("An Error occurred, but at what state?");
    };
    Parallel.For(-100, 100, (state) =>
	{
	    lastKnownState = state;
	    if (state == 42)
	    {   
		throw new ArgumentException("Some error with state #42");
	    }
	}
    );
    Thread.Sleep(TimeSpan.FromSeconds(5));
}

In this case, “lastKnownState” returned as 99, not 42. It may be a different value on your computer, but it’s important to note that it’s not correct. Since the lastKnownState gets overwritten by multiple threads, and so it’s not thread-safe.

So Attempt #2, let’s create a concurrent Dictionary, store the states associated with each thread, then lookup the state within the FirstChanceExceptionHandler.

public static ConcurrentDictionary<int,int> ThreadStates = new ConcurrentDictionary<int, int>();
static void Main(string[] args)
{
    AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
    {
	var ok = ThreadStates.TryGetValue(Thread.CurrentThread.ManagedThreadId,out var state);
	if (ok) Console.WriteLine($"An error occurred at state {state}");
    };
    Parallel.For(-100, 100, state =>
	{
	    ThreadStates.TryAdd(Thread.CurrentThread.ManagedThreadId, state);
	    if (state == 42)
	    {	      
		throw new ArgumentException("Some error with state #42");
	    }  
	}
    );
    Thread.Sleep(TimeSpan.FromSeconds(5));
}

This looks right, but, it’s not. The state is still incorrect within the Exception Handler? Why is this. Well, because Parallel.For will recycle threads. Which means that the “ManagedThreadId” can be the same for multiple states. The TryAdd will ignore duplicates, so data will be lost.

public static ConcurrentDictionary<int,int> ThreadStates = new ConcurrentDictionary<int, int>();
static void Main(string[] args)
{
    AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
    {
	var ok = ThreadStates.TryGetValue(Thread.CurrentThread.ManagedThreadId,out var state);
	if (ok) Console.WriteLine($"An error occurred at state {state}");
    };
    Parallel.For(-100, 100, state =>
	{
	    ThreadStates.TryAdd(Thread.CurrentThread.ManagedThreadId, state);
	    if (state == 42)
	    {
	      
		throw new ArgumentException("Some error with state #42");
	    }
	    ThreadStates.TryRemove(Thread.CurrentThread.ManagedThreadId, out var oldState);

	}
    );
    Thread.Sleep(TimeSpan.FromSeconds(5));
}

So, the trick is to remove the state from the ThreadStates collection once complete, using the code highlighted in bold.

Now, that’s working, let’s refactor the code to keep the specifics of the code separate from the main logic flow. So, I’m creating a class called ThreadContext as follows;

public sealed class ThreadContext<T> : IDisposable
{
	private static readonly ConcurrentDictionary<int, T> ThreadStates = new ConcurrentDictionary<int, T>();

	public static T RetrieveState()
	{
	    var ok = ThreadStates.TryGetValue(Thread.CurrentThread.ManagedThreadId, out var state);
	    return ok ? state : default;
	}

	public void Dispose()
	{
	    ThreadStates.TryRemove(Thread.CurrentThread.ManagedThreadId, out var oldState);
	}

	public ThreadContext(T state)
	{
	    ThreadStates.TryAdd(Thread.CurrentThread.ManagedThreadId, state);
	}
}

Notice, that the class is now generic, so that it can hold any nullable object – like a string, a nullable int, or something much more complex.

Now the Main method becomes

AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
{
	var state = ThreadContext<int?>.RetrieveState();

	if (state != null) Console.WriteLine($"An error occurred at state {state}");
};
Parallel.For(-100, 100, state =>
{
    using (new ThreadContext<int?>(state))
    {

	if (state == 42)
	{
	    throw new ArgumentException("Some error with state #42");
	}
    }

}
);
Thread.Sleep(TimeSpan.FromSeconds(5));

Now to go try this on a real project …

Categories: Uncategorized

Leave a comment