Fire and forget a Task in .NET

 
 
  • Gérald Barré

Tasks are very useful to start background work. You can use Task.Run(() => { ... }) to start a background operation. If the background operation succeeds, everything's good. If it fails, you check the task.Exception directly to know why it failed or you can await the task or use a blocking call such as Result or Wait() to get the exception. In this case you can handle the exception the way you want. If you don't watch the result of the task, the exception will eventually be raised using TaskScheduler.UnobservedTaskException. You can read more about this behavior in this post written by Stephen Toub a few years ago.

Sometimes you just want to start the task and don't wait for it nor want to know if it succeeds or not. If you just use Task.Run and the task fails, you still need to handle the exception to avoid the UnobservedTaskException event to be raised. The idea is to create an extension method to forget the task. There are some performance tricks explained in the comments.

C#
public static class TaskExtensions
{
    /// <summary>
    /// Observes the task to avoid the UnobservedTaskException event to be raised.
    /// </summary>
    public static void Forget(this Task task)
    {
        // note: this code is inspired by a tweet from Ben Adams: https://twitter.com/ben_a_adams/status/1045060828700037125
        // Only care about tasks that may fault (not completed) or are faulted,
        // so fast-path for SuccessfullyCompleted and Canceled tasks.
        if (!task.IsCompleted || task.IsFaulted)
        {
            // use "_" (Discard operation) to remove the warning IDE0058: Because this call is not awaited, execution of the current method continues before the call is completed
            // https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards?WT.mc_id=DT-MVP-5003978#a-standalone-discard
            _ = ForgetAwaited(task);
        }

        // Allocate the async/await state machine only when needed for performance reasons.
        // More info about the state machine: https://blogs.msdn.microsoft.com/seteplia/2017/11/30/dissecting-the-async-methods-in-c/?WT.mc_id=DT-MVP-5003978
        async static Task ForgetAwaited(Task task)
        {
            try
            {
                // No need to resume on the original SynchronizationContext, so use ConfigureAwait(false)
                await task.ConfigureAwait(false);
            }
            catch
            {
                // Nothing to do here
            }
        }
    }
}

You can use this extension method like that:

C#
Task.Run(() => { ... }).Forget();

This extension method is part of Meziantou.Framework (GitHub, NuGet). You can add the NuGet package to your project and start using the Forget extension method and all the useful extension methods part of this package.

csproj (MSBuild project file)
<PackageReference Include="Meziantou.Framework" Version="2.7.6" />

#Alternative implementation

You can remove the try/catch by creating a custom Task awaiter.

C#
public static class TaskExtensions
{
    public static void Forget(this Task task)
    {
        if (!task.IsCompleted || task.IsFaulted)
        {
            _ = ForgetAwaited(task);
        }

        async static Task ForgetAwaited(Task task)
        {
            await new NoThrowAwaiter(task);
        }
    }

    // From https://github.com/dotnet/aspnetcore/blob/bae39d367cf8b92a9fb004fd9515bd3e2d0a46bf/src/SignalR/common/Http.Connections/src/Internal/TaskExtensions.cs
    private readonly struct NoThrowAwaiter : ICriticalNotifyCompletion
    {
        private readonly Task _task;
        public NoThrowAwaiter(Task task) { _task = task; }
        public NoThrowAwaiter GetAwaiter() => this;
        public bool IsCompleted => _task.IsCompleted;
        public void GetResult() { _ = _task.Exception; } // Observe exception
        public void OnCompleted(Action continuation) => _task.GetAwaiter().OnCompleted(continuation);
        public void UnsafeOnCompleted(Action continuation) => _task.GetAwaiter().UnsafeOnCompleted(continuation);
    }
}

#Alternative implementation (.NET 8)

Starting with .NET 8, you can remove the NoThrowAwaiter and use ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing) (documentation):

C#
public static class TaskExtensions
{
    public static void Forget(this Task task)
    {
        if (!task.IsCompleted || task.IsFaulted)
        {
            _ = ForgetAwaited(task);
        }

        async static Task ForgetAwaited(Task task)
        {
            await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
        }
    }
}

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub