Fire and forget a Task in .NET

 
 
  • Gérald Barré

Tasks are useful for starting background work. You can use Task.Run(() => { ... }) to start a background operation. If it succeeds, no further action is needed. If it fails, you can inspect task.Exception directly, await the task, or use a blocking call such as Result or Wait() to retrieve the exception and handle it as needed. If you do not observe the result of the task, the exception will eventually be raised via TaskScheduler.UnobservedTaskException. You can read more about this behavior in this post written by Stephen Toub.

Sometimes you want to start a task and neither wait for it nor care whether it succeeds. However, if the task fails and you used a plain Task.Run, you still need to observe the exception to prevent the UnobservedTaskException event from being raised. The solution is to create a Forget extension method that silently observes the task. Performance optimizations are explained in the code 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 this:

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

This extension method is available in the Meziantou.Framework package (GitHub, NuGet). Add the package to your project to use Forget along with many other helpful extension methods.

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

#Alternative implementation

You can remove the try/catch block 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?