Skip to content

Async C sharp

This was quite the gotcha while I was developing, and I’d used C# for years already at that point. Microsoft has a bunch of information here for how to handle exceptions from Tasks, and there’s even this section on that page about unobserved exceptions.

In short, unobserved exceptions are not reported anywhere by default. E.g. this code (fiddle here) will not log any exception despite that one is hit:

using System;
using System.Threading.Tasks;
public class Program
{
public static void Main()
{
_ = HitsException();
System.Threading.Thread.Sleep(1000);
Console.WriteLine("Quitting");
}
private static async Task HitsException() {
string? foo = null;
foo.ToLower(); // this won't print the exception since we don't await the call
Console.WriteLine("Code is beyond the exception"); // this won't print
}
}

The “proper” way to fix this is simply to await your call, but sometimes, you want to trigger a task and let it run in the background. For those cases, adding a FireAndForget extension to Task is a good idea:

using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Godot;
namespace Game.Runtime.CSharp.Extensions;
public static class TaskExtensions
{
extension(Task task)
{
public void FireAndForget([CallerMemberName] string operationName = "")
{
Observe(task, operationName);
}
}
private static async void Observe(Task task, string operationName)
{
try
{
await task.ConfigureAwait(true);
}
catch (OperationCanceledException)
{
}
catch (Exception e)
{
GD.PushError($"Detached async operation '{operationName}' failed:\n{e}");
// ReSharper disable once AsyncVoidThrowException - we WANT this to crash the process
throw;
}
}
}

You can then call this function like so: SomeAsyncFunctionThatYouDoNotAwait().FireAndForget();. It will automatically plumb in the caller name thanks to CallerMemberName so that exceptions are much clearer.

If all you want is to see the unobserved exceptions though, you can add a handler and then make sure the Task is both garbage-collected and finalized:

using System;
using System.Threading;
using System.Threading.Tasks;
public class Program
{
public static void Main()
{
TaskScheduler.UnobservedTaskException += (_, e) =>
{
Console.Error.WriteLine(e.Exception);
e.SetObserved();
};
FireAndForget();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Thread.Sleep(1000);
Console.WriteLine("Quitting");
}
private static void FireAndForget()
{
_ = HitsException();
}
private static async Task HitsException()
{
string? foo = null;
foo.ToLower();
}
}