AsyncAwaitBestPractices

Extensions for System.Threading.Tasks.Task
.
Inspired by John Thiriet's blog posts:
AsyncAwaitBestPractices

Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices/
SafeFireAndForget
- An extension method to safely fire-and-forget a
Task
or a ValueTask
- Ensures the
Task
will rethrow an Exception
if an Exception
is caught in IAsyncStateMachine.MoveNext()
WeakEventManager
- Avoids memory leaks when events are not unsubscribed
- Used by
AsyncCommand
, AsyncCommand<T>
, AsyncValueCommand
, AsyncValueCommand<T>
- Usage instructions
AsyncAwaitBestPractices.MVVM

-
Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/
-
Allows for Task
to safely be used asynchronously with ICommand
:
IAsyncCommand : ICommand
AsyncCommand : IAsyncCommand
IAsyncCommand<T> : ICommand
AsyncCommand<T> : IAsyncCommand<T>
IAsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute>
AsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute, TCanExecute>
-
Allows for ValueTask
to safely be used asynchronously with ICommand
:
IAsyncValueCommand : ICommand
AsyncValueCommand : IAsyncValueCommand
IAsyncValueCommand<T> : ICommand
AsyncValueCommand<T> : IAsyncValueCommand<T>
IAsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute>
AsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute, TCanExecute>
-
Usage instructions
Setup
AsyncAwaitBestPractices
AsyncAwaitBestPractices.MVVM
Why Do I Need This?
Podcasts
No Dogma Podcast, Hosted by Bryan Hogan
Video
NDC London 2024
Correcting Common Async Await Mistakes in .NET 8

Explaination
Async/await is great but there are two subtle problems that can easily creep into code:
- Creating race conditions/concurrent execution (where you code things in the right order but the code executes in a different order than you expect)
- Creating methods where the compiler recognizes exceptions but you the coder never see them (making it head-scratchingly annoying to debug especially if you accidentally introduced a race condition that you can’t see).
This library solves both of these problems.
To better understand why this library was created and the problem it solves, it’s important to first understand how the compiler generates code for an async method.
tl;dr A non-awaited Task
doesn't rethrow exceptions and AsyncAwaitBestPractices.SafeFireAndForget
ensures it will
Compiler-Generated Code for Async Method

(Source: Xamarin University: Using Async and Await)
The compiler transforms an async
method into an IAsyncStateMachine
class which allows the .NET Runtime to "remember" what the method has accomplished.

(Source: Xamarin University: Using Async and Await)
The IAsyncStateMachine
interface implements MoveNext()
, a method the executes every time the await
operator is used inside of the async
method.
MoveNext()
essentially runs your code until it reaches an await
statement, then it return
s while the await
'd method executes. This is the mechanism that allows the current method to "pause", yielding its thread execution to another thread/Task.
Try/Catch in MoveNext()
Look closely at MoveNext()
; notice that it is wrapped in a try/catch
block.
Because the compiler creates IAsyncStateMachine
for every async
method and MoveNext()
is always wrapped in a try/catch
, every exception thrown inside of an async
method is caught!
How to Rethrow an Exception Caught By MoveNext
Now we see that the async
method catches every exception thrown - that is to say, the exception is caught internally by the state machine, but you the coder will not see it. In order for you to see it, you'll need to rethrow the exception to surface it in your debugging. So the questions is - how do I rethrow the exception?
There are a few ways to rethrow exceptions that are thrown in an async
method:
- Use the
await
keyword (Prefered)
- e.g.
await DoSomethingAsync()
- Use
.GetAwaiter().GetResult()
- e.g.
DoSomethingAsync().GetAwaiter().GetResult()
The await
keyword is preferred because await
allows the Task
to run asynchronously on a different thread, and it will not lock-up the current thread.
What About .Result
or .Wait()
?
Never, never, never, never, never use .Result
or .Wait()
:
-
Both .Result
and .Wait()
will lock-up the current thread. If the current thread is the Main Thread (also known as the UI Thread), your UI will freeze until the Task
has completed.
-
.Result
or .Wait()
rethrow your exception as a System.AggregateException
, which makes it difficult to find the actual exception.
Usage
AsyncAwaitBestPractices
SafeFireAndForget
An extension method to safely fire-and-forget a Task
.
SafeFireAndForget
allows a Task to safely run on a different thread while the calling thread does not wait for its completion.
public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, System.Action<System.Exception>? onException = null, bool continueOnCapturedContext = false)
public static async void SafeFireAndForget(this System.Threading.Tasks.ValueTask task, System.Action<System.Exception>? onException = null, bool continueOnCapturedContext = false)
On .NET 8.0 (and higher)
.NET 8.0 Introduces ConfigureAwaitOptions
that allow users to customize the behavior when awaiting:
ConfigureAwaitOptions.None
ConfigureAwaitOptions.SuppressThrowing
- Avoids throwing an exception at the completion of awaiting a Task that ends in the Faulted or Canceled state
ConfigureAwaitOptions.ContinueOnCapturedContext
- Attempts to marshal the continuation back to the original SynchronizationContext or TaskScheduler present on the originating thread at the time of the await
ConfigureAwaitOptions.ForceYielding
- Forces an await on an already completed Task to behave as if the Task wasn't yet completed, such that the current asynchronous method will be forced to yield its execution
For more information, check out Stephen Cleary's blog post, "ConfigureAwait in .NET 8".
public static void SafeFireAndForget(this System.Threading.Tasks.Task task, ConfigureAwaitOptions configureAwaitOptions, Action<Exception>? onException = null)
Basic Usage - Task
void HandleButtonTapped(object sender, EventArgs e)
{
ExampleAsyncMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex));
}
async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
}
Note: ConfigureAwaitOptions.SuppressThrowing
will always supress exceptions from being rethrown. This means that onException
will never execute when ConfigureAwaitOptions.SuppressThrowing
is set.
Basic Usage - ValueTask
If you're new to ValueTask, check out this great write-up, Understanding the Whys, Whats, and Whens of ValueTask
.
void HandleButtonTapped(object sender, EventArgs e)
{
ExampleValueTaskMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex));
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
}
Advanced Usage
void InitializeSafeFireAndForget()
{
SafeFireAndForgetExtensions.Initialize(shouldAlwaysRethrowException: false);
SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => Console.WriteLine(ex));
}
void UninitializeSafeFireAndForget()
{
SafeFireAndForgetExtensions.RemoveDefaultExceptionHandling();
}
void HandleButtonTapped(object sender, EventArgs e)
{
ExampleAsyncMethod().SafeFireAndForget<WebException>(onException: ex =>
{
if(ex.Response is HttpWebResponse webResponse)
Console.WriteLine($"Task Exception\n Status Code: {webResponse.StatusCode}");
});
ExampleValueTaskMethod().SafeFireAndForget<WebException>(onException: ex =>
{
if(ex.Response is HttpWebResponse webResponse)
Console.WriteLine($"ValueTask Error\n Status Code: {webResponse.StatusCode}");
});
}
async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
throw new WebException();
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
throw new WebException();
}
Note: ConfigureAwaitOptions.SuppressThrowing
will always supress exceptions from being rethrown. This means that onException
will never execute when ConfigureAwaitOptions.SuppressThrowing
is set.
WeakEventManager
An event implementation that enables the garbage collector to collect an object without needing to unsubscribe event handlers.
Inspired by Xamarin.Forms.WeakEventManager.
Using EventHandler
readonly WeakEventManager _canExecuteChangedEventManager = new WeakEventManager();
public event EventHandler CanExecuteChanged
{
add => _canExecuteChangedEventManager.AddEventHandler(value);
remove => _canExecuteChangedEventManager.RemoveEventHandler(value);
}
void OnCanExecuteChanged() => _canExecuteChangedEventManager.RaiseEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));
Using Delegate
readonly WeakEventManager _propertyChangedEventManager = new WeakEventManager();
public event PropertyChangedEventHandler PropertyChanged
{
add => _propertyChangedEventManager.AddEventHandler(value);
remove => _propertyChangedEventManager.RemoveEventHandler(value);
}
void OnPropertyChanged([CallerMemberName]string propertyName = "") => _propertyChangedEventManager.RaiseEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged));
Using Action
readonly WeakEventManager _weakActionEventManager = new WeakEventManager();
public event Action ActionEvent
{
add => _weakActionEventManager.AddEventHandler(value);
remove => _weakActionEventManager.RemoveEventHandler(value);
}
void OnActionEvent(string message) => _weakActionEventManager.RaiseEvent(message, nameof(ActionEvent));
WeakEventManager<T>
An event implementation that enables the garbage collector to collect an object without needing to unsubscribe event handlers.
Inspired by Xamarin.Forms.WeakEventManager.
Using EventHandler<T>
readonly WeakEventManager<string> _errorOcurredEventManager = new WeakEventManager<string>();
public event EventHandler<string> ErrorOcurred
{
add => _errorOcurredEventManager.AddEventHandler(value);
remove => _errorOcurredEventManager.RemoveEventHandler(value);
}
void OnErrorOcurred(string message) => _errorOcurredEventManager.RaiseEvent(this, message, nameof(ErrorOcurred));
Using Action<T>
readonly WeakEventManager<string> _weakActionEventManager = new WeakEventManager<string>();
public event Action<string> ActionEvent
{
add => _weakActionEventManager.AddEventHandler(value);
remove => _weakActionEventManager.RemoveEventHandler(value);
}
void OnActionEvent(string message) => _weakActionEventManager.RaiseEvent(message, nameof(ActionEvent));
AsyncAwaitBestPractices.MVVM
AsyncCommand
Allows for Task
to safely be used asynchronously with ICommand
:
AsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute, TCanExecute>
IAsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute>
AsyncCommand<T> : IAsyncCommand<T>
IAsyncCommand<T> : ICommand
AsyncCommand : IAsyncCommand
IAsyncCommand : ICommand
public AsyncCommand(Func<TExecute, Task> execute,
Func<TCanExecute, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncCommand(Func<T, Task> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncCommand(Func<Task> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public class ExampleClass
{
bool _isBusy;
public ExampleClass()
{
ExampleAsyncCommand = new AsyncCommand(ExampleAsyncMethod);
ExampleAsyncIntCommand = new AsyncCommand<int>(ExampleAsyncMethodWithIntParameter);
ExampleAsyncIntCommandWithCanExecute = new AsyncCommand<int, int>(ExampleAsyncMethodWithIntParameter, CanExecuteInt);
ExampleAsyncExceptionCommand = new AsyncCommand(ExampleAsyncMethodWithException, onException: ex => Console.WriteLine(ex.ToString()));
ExampleAsyncCommandWithCanExecuteChanged = new AsyncCommand(ExampleAsyncMethod, _ => !IsBusy);
ExampleAsyncCommandReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: true);
}
public IAsyncCommand ExampleAsyncCommand { get; }
public IAsyncCommand<int> ExampleAsyncIntCommand { get; }
public IAsyncCommand<int, int> ExampleAsyncIntCommandWithCanExecute { get; }
public IAsyncCommand ExampleAsyncExceptionCommand { get; }
public IAsyncCommand ExampleAsyncCommandWithCanExecuteChanged { get; }
public IAsyncCommand ExampleAsyncCommandReturningToTheCallingThread { get; }
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy != value)
{
_isBusy = value;
ExampleAsyncCommandWithCanExecuteChanged.RaiseCanExecuteChanged();
}
}
}
async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
}
async Task ExampleAsyncMethodWithIntParameter(int parameter)
{
await Task.Delay(parameter);
}
async Task ExampleAsyncMethodWithException()
{
await Task.Delay(1000);
throw new Exception();
}
bool CanExecuteInt(int count)
{
if(count > 2)
return true;
return false;
}
void ExecuteCommands()
{
_isBusy = true;
try
{
ExampleAsyncCommand.Execute(null);
ExampleAsyncIntCommand.Execute(1000);
ExampleAsyncExceptionCommand.Execute(null);
ExampleAsyncCommandReturningToTheCallingThread.Execute(null);
if(ExampleAsyncCommandWithCanExecuteChanged.CanExecute(null))
ExampleAsyncCommandWithCanExecuteChanged.Execute(null);
if(ExampleAsyncIntCommandWithCanExecute.CanExecute(1))
ExampleAsyncIntCommandWithCanExecute.Execute(1);
}
finally
{
_isBusy = false;
}
}
}
AsyncValueCommand
Allows for ValueTask
to safely be used asynchronously with ICommand
.
If you're new to ValueTask, check out this great write-up, Understanding the Whys, Whats, and Whens of ValueTask
.
AsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute, TCanExecute>
IAsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute>
AsyncValueCommand<T> : IAsyncValueCommand<T>
IAsyncValueCommand<T> : ICommand
AsyncValueCommand : IAsyncValueCommand
IAsyncValueCommand : ICommand
public AsyncValueCommand(Func<TExecute, ValueTask> execute,
Func<TCanExecute, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncValueCommand(Func<T, ValueTask> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncValueCommand(Func<ValueTask> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public class ExampleClass
{
bool _isBusy;
public ExampleClass()
{
ExampleValueTaskCommand = new AsyncValueCommand(ExampleValueTaskMethod);
ExampleValueTaskIntCommand = new AsyncValueCommand<int>(ExampleValueTaskMethodWithIntParameter);
ExampleValueTaskIntCommandWithCanExecute = new AsyncValueCommand<int, int>(ExampleValueTaskMethodWithIntParameter, CanExecuteInt);
ExampleValueTaskExceptionCommand = new AsyncValueCommand(ExampleValueTaskMethodWithException, onException: ex => Debug.WriteLine(ex.ToString()));
ExampleValueTaskCommandWithCanExecuteChanged = new AsyncValueCommand(ExampleValueTaskMethod, _ => !IsBusy);
ExampleValueTaskCommandReturningToTheCallingThread = new AsyncValueCommand(ExampleValueTaskMethod, continueOnCapturedContext: true);
}
public IAsyncValueCommand ExampleValueTaskCommand { get; }
public IAsyncValueCommand<int> ExampleValueTaskIntCommand { get; }
public IAsyncCommand<int, int> ExampleValueTaskIntCommandWithCanExecute { get; }
public IAsyncValueCommand ExampleValueTaskExceptionCommand { get; }
public IAsyncValueCommand ExampleValueTaskCommandWithCanExecuteChanged { get; }
public IAsyncValueCommand ExampleValueTaskCommandReturningToTheCallingThread { get; }
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy != value)
{
_isBusy = value;
ExampleValueTaskCommandWithCanExecuteChanged.RaiseCanExecuteChanged();
}
}
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
}
async ValueTask ExampleValueTaskMethodWithIntParameter(int parameter)
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(parameter);
}
async ValueTask ExampleValueTaskMethodWithException()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
throw new Exception();
}
bool CanExecuteInt(int count)
{
if(count > 2)
return true;
return false;
}
void ExecuteCommands()
{
_isBusy = true;
try
{
ExampleValueTaskCommand.Execute(null);
ExampleValueTaskIntCommand.Execute(1000);
ExampleValueTaskExceptionCommand.Execute(null);
ExampleValueTaskCommandReturningToTheCallingThread.Execute(null);
if (ExampleValueTaskCommandWithCanExecuteChanged.CanExecute(null))
ExampleValueTaskCommandWithCanExecuteChanged.Execute(null);
if(ExampleValueTaskIntCommandWithCanExecute.CanExecute(2))
ExampleValueTaskIntCommandWithCanExecute.Execute(2);
}
finally
{
_isBusy = false;
}
}
}
Learn More