serlingpa
serlingpa

Reputation: 12710

Exceptions in C# not behaving as expected

I am developing an ASP.NET Core 3.1 API for my React front end app.

My problem is that my exceptions are not trickling up through my object hierarchy as I expect. I think it might be due to some multi-threading issue, but I don't know enough about C# to be sure! I am learning on Pluralsight, but I'm currently studying networking, which is not going to help me with this!

The calling code is a SignalR Hub method, that looks like this:

public async Task<bool> UpdateProfile(object profileDto)
{
  try
  {
    ProfileDTO profile = ((JsonElement) profileDto).ToObject<ProfileDTO>();
    _profile.UpdateProfile(profile);
    return true;
  }
  catch (Exception e)
  {
    return false;
  }
}

I would expect that any exceptions thrown or not handled in _profile.UpdateProfile(profile); would cause the exception block here to return false. My UpdateProfile() looks like this:

public void UpdateProfile(ProfileDTO profileDto)
{
  _databaseService.ExecuteInTransaction(async session =>
  {
    // simulate an error
    throw new Exception("Some exception");
  });
}

...and my ExecuteInTransaction() looks like this:

public async void ExecuteInTransaction(Func<IClientSessionHandle, Task> databaseAction)
{
  using var session = await Client.StartSessionAsync();

  try
  {
    session.StartTransaction();
    await databaseAction(session);
    await session.CommitTransactionAsync();
  }
  catch(Exception e)
  {
    await session.AbortTransactionAsync();
    throw e;
  }
}

I would expect that the exception thrown in UpdateProfile() would trickle up to the catch block in ExecuteInTransaction() — which it does — but then further, I would expect this exception to trickle up to the Hub UpdateProfile() method. Instead, it ends up in the Throw() method on the ExceptionDispatchInfo class in the System.Runtime.ExceptionServices namespace.

Reading through the comments in this file makes me think it is a threading issue, but I don't know enough about how threading works yet in C#. Is it possible for the exceptions thrown in UpdateProfile() to make it up to the top level of my Hub UpdateProfile()? (just noticed they confusingly have the same name).

Upvotes: 0

Views: 130

Answers (1)

JohanP
JohanP

Reputation: 5472

Your problem is the async void signature of ExecuteInTransaction.

Async void methods have different error-handling semantics. When an exception is thrown out of an async Task or async Task method, that exception is captured and placed on the Task object. With async void methods, there is no Task object, so any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started Source

What this means, if you're using ASP.NET Core where there is no SynchronizationContext, your exception will be thrown most likely on a threadpool thread if you didn't mess around with the TaskScheduler. If you're on older .NET framework code, it will be on the captured context but either way, what you know about exception handling does not apply here. You can catch these exceptions by subscribing to AppDomain.UnhandledException but no one wants to do that in maintainable code.

To fix this, change public async void ExecuteInTransaction to public async Task ExecuteInTransaction, change public void UpdateProfile to public async Task UpdateProfile and call it like so:

public async Task<bool> UpdateProfile(object profileDto)
{
  try
  {
    ProfileDTO profile = ((JsonElement) profileDto).ToObject<ProfileDTO>();
    await _profile.UpdateProfile(profile);
    return true;
  }
  catch (Exception e)
  {
    return false;
  }
}

public async Task UpdateProfile(ProfileDTO profileDto)
{
  await _databaseService.ExecuteInTransaction(async session =>
  {
    // simulate an error
    throw new Exception("Some exception");
  });
}

Upvotes: 3

Related Questions