Async tips and best practices

Async/await is arguably one of the most important language improvement to ever come to C#. But with most great powers comes great responsibility. Designing APIs that leverage async/await can seem easy at first but if you don’t impose a few standards during the power dev phase of a project, when it comes time to support things like suspend/resume & robust navigation, you may quickly find yourself in a quagmire with a host of clean-up work to do. Here are a few tips me and my teams have come up with over the years to help start off on the right foot when building APIs.

Naming conventions

This is a simple and somewhat superficial one but worth mentioning. WinRT has defined a naming convention for async methods by always using the “Async” suffix at the end of a method name. This helps inform consumers of an API that it is asynchronous without having to look at the return type. It is also nice for auditing an app before shipping to ensure there are no hard to duplicate race conditions and that an app will suspend properly at any moment. More on these concerns later.

Should I add a cancellation token parameter?

Assuming you are returning a Task rather than IAsyncAction or IAsyncOperation (more on these later). Assume the answer is always yes. Async API take time to run. Maybe you already know that an async API will never take more than a few milliseconds, but this is an implementation detail that should never be imposed by the method signature. My advice is to always assume when designing an API that it might take many seconds and that the caller may want to abort the operation. This helps future proof your APIs in the following ways:

Ultimately you may abstract your class and support different implementations that take longer than the original implementation. Some APIs like file system IO may always be quick in your dev bubble, but you never know when someone is loading from a network location or USB 1.0 thumb drive. Plan for the worst.

Furthermore, the consumer of the API should never need to care about how long an API will take to run. Consumers should always design with “what happens if this takes 2 seconds to run” in mind and having a consistent pattern to work with will help ensure this mind set.

WinRT compatibility: Cancellation tokens with IAsyncOperations and IAsyncActions

You should never need a cancellation token param when you are returning an IAsyncOperation or IAsyncAction. Note: These WinRT types that are required when exposing async operations from WinRT compatible components; Task is not permitted since it is a .NET specific type. The good news is, IAsyncOperation and IAsyncAction both provide cancellation token internally as part of the type. The easiest way IMO to build async WinRT components is to create a public method that returns the IAsync* type, and a private method that returns Task. Use the AsyncInfo static type to help convert a task into an IAsync* type AND supply the cancellation token. For example:

public IAsyncAction DoSomethingAsync() { return AsyncInfo.Run(c => DoSomethingAsync(c)); } private async Task DoSomethingAsync(CancellationToken cancellationToken) { await Task.Delay(1000, cancellationToken); }

The consumer of the public API should then always supply a cancellation token via the .AsTask(CancellationToken) extension method overload. For example:

await DoSomethingAsync().AsTask(myCancellationToken);

Optional parameters and cancellation tokens

Optional parameters can be convenient but the CancellationToken parameter should always be last. However, as you know, optional parameters cannot be followed by non-optional parameters. Fortunately, there’s a way to also make the CancellationToken parameter optional.

Don’t do this:

async Task DoSomethingAsync(CancellationToken cancellationToken, int waitTimeInSec = 1) { await Task.Delay(TimeSpan.FromSeconds(waitTimeInSec), cancellationToken); }

Do this instead:

async Task DoSomethingAsync(int waitTimeInSec = 1, CancellationToken cancellationToken = default(CancellationToken)) { await Task.Delay(TimeSpan.FromSeconds(waitTimeInSec), cancellationToken); }

Note: this is getting pretty picky and you may chose to ignore this advice but it is my personal preference to stick with the established Microsoft convention by keeping CancellationToken at the end.

To throw or not to throw, that is the question.

This is the most important tip of all IMO. If your method accepts a cancellation token, it should always throw an OperationCancelledException if that token is cancelled during the operation. NEVER swallow that exception. If the consumer cancels the operation via the supplied cancellation token, assume they need to know and do this by throwing. The easiest way is to just let the exception bubble back up stream from any async methods you are calling. For example, here, we can assume Task.Delay will throw if the cancellation token is cancelled. We don’t need to do anything except not catch the exception.

async Task DoSomethingAsync(TimeSpan waitTime, CancellationToken cancellationToken) { await Task.Delay(waitTime, cancellationToken); }

If you can’t rely on the async calls you are making internally within your method to throw or you need to handle cancellations manually such as in the case of transactions, simply call cancellationToken.ThrowIfCancellationRequested(). However, usually this is not required.

Should I throw even if the method finishes?

What if a method successfully completes but the cancellationToken is flagged after the last async operation? For example:

async Task<int> GetAverageGradeAsync(CancellationToken cancellationToken) { var allScores = await GetAllScoresAsync(cancellationToken); int total = 0; int sum = 0; foreach (var score in allScores) { // SHOULD I THROW HERE? total++; // OR HERE? sum += score; } // WHAT ABOUT HERE? return sum / total; }

This depends… is the method intended to be thread-safe? Does it take a long time to run?

In most cases, one can assume that the cancellation token will be flagged on the same thread as the loop is running on. If this is the case, there is no benefit to monitoring the cancellation token once the original async operation has completed. IF you can make these assumptions, there is no possible way that the cancellation token could even be set to a cancelled state in between these lines of code since everything is happening on the same thread. Yes, I know that ideally one would design an API to be thread-safe and not make assumptions about what thread the cancellation token is flagged from. However, there is a performance cost to checking the cancellation token and there’s a productivity and maintenance cost to inserting a bunch of ugly cancellationToken.ThrowIfCancellationRequested() calls in your code. My vote in most cases is to favor readability and leave out the calls to ThrowIfCancellationRequested.

That said, if you have a billion scores in the example above, ignoring the potential for an overflow, this could take a long time to calculate. In this case, you may want to run your calculation on another thread and handle cancellations during the calculation.

First, the best way to do this is on the same thread as the async operation. Creating new threads and hopping between synchronization contexts can be expensive. To prevent a new thread from being created, use Task’s ContinueWith method. This ensures that the same thread that the async operation was ran on is used for your subsequent code. Additionally, call cancellationToken.ThrowIfCancellationRequested() in your continuation code but call sparingly to improve performance. For example:

async Task<int> GetAverageGradeAsync(CancellationToken cancellationToken) { return await GetAllScoresAsync(cancellationToken).ContinueWith(t => { int total = 0; int sum = 0; foreach (var score in t.Result) { total++; sum += score; if (total % 1000 == 0) cancellationToken.ThrowIfCancellationRequested(); } cancellationToken.ThrowIfCancellationRequested(); return sum / total; }, TaskContinuationOptions.OnlyOnRanToCompletion); }

Please note that in the above example, it is possible that the cancellationToken was flagged as cancelled after the last call to cancellationToken.ThrowIfCancellationRequested was made but before or during sum / total was executed. This could result in your method successfully returning even though cancellationToken was set. This is OK and consumers of your API should expect that cancellation tokens are not always honored by async methods when dealing with multi-threaded scenarios.

Should I throw internal cancellation tokens?

No. If your async API creates and flags its own cancellation token, do not surface this to the consumer of your API by throwing an OperationCancelledException. A great example of this is for a timeout. Imagine you have an API that makes a web request with a 5 sec timeout. Also imagine that the timeout is something we want to handle internally within the API. A great way to impose timeouts is to create a CancellationTokenSource object and supply a timeout via the constructor. Build a linked cancellation token that marries the cancellation token supplied to the method with the timeout token created within the method and use during your web request operation. Then, check to see which one was cancelled and only bubble up OperationCancelledException if the token passed to your method was cancelled. Otherwise, throw a new exception type or handle as you see fit. For example:

static async Task<string> GetWebResponseAsync(Uri uri, CancellationToken cancellationToken) { // Adding a Cancellation token to time out the network request CancellationTokenSource timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); CancellationToken requestCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token).Token; using (var httpClient = new HttpClient()) { try { return await httpClient.GetStringAsync(uri).AsTask(requestCancellationToken); } catch (OperationCanceledException) when (timeoutTokenSource.IsCancellationRequested) { // request timed out throw new TimeoutException(); } } }

What exception should be caught?

When catching cancellation exceptions, always catch OperationCanceledException, not TaskCanceledException. TaskCanceledException inherits OperationCanceledException but there are other exception types that can derive from it as well. OperationCanceledException is safer to catch unless you know you only want to TaskCanceledException.

How do I handle transactions?

If your async method throws an OperationCanceledException always be sure that your app’s state is not left broken. Either don’t throw at all and force the method to complete entirely or roll back your changes. A good example of this would be if you are writing 2 files to disk that both need to be completely written. In this case, if you threw an exception after writing 1 file but before writing the next, you would leave the app in a broken state. Tip: With C# 6 you can put additional async code in the catch block which makes it easier to roll back transactions if necessary.

To catch or not to catch

As mentioned earlier, you should always let OperationCanceledExceptions bubble up as long as they are the direct result of the CancellationToken supplied as a parameter being canceled. However, if you are creating or re-using a CancellationToken within your code and passing that to an async method, you should not throw OperationCanceledException from your method since the caller has an implicit guarantee that they are not responsible for cancelling the operation by virtue of not supplying a cancellation token. A simple scenario would be in the case of a button click event that needs to run an async operation. Here you might want to disable the button so it can’t get clicked again during the async operation, execute the async operation supplying it with a cancellation token of some kind (perhaps one that is set when the app suspends) and catch and ignore OperationCanceledException. In the finally clause you would restore the button state to ensure you always leave the app in a good state.

private async void Button_Click(object sender, RoutedEventArgs e) { var control = (Control)sender; control.IsEnabled = false; try { await DoSomethingAsync(((App)Application.Current).CancellationToken); } catch (OperationCanceledException) { /* ignore, suspending */ } finally { control.IsEnabled = true; } }

Note, you may want to supply CancellationToken.None if there is never a scenario that should be captured. The async API will not need to be changed to support cancellations in other scenarios.

If you follow this guideline and the one from earlier about always accepting a CancellationToken parameter in your async APIs, you will have the net result of allowing cancellation tokens to be supplied at the highest level and also be caught at that same level. This gives you the greatest flexibility for code reuse. For example, you might have an API that is called from a button click and from a background task. The outcome of cancelling may be very different for these two scenarios. In the case of the button click, you may want to show the user an error. In the result of the background task you may want to just ignore it. Building your APIs in the ways described above gives your consumers the ability to choose how to handle cancellations.

async void

There are a lot of articles on the web that say “Don’t do it” so I won’t go into all the pitfalls here and completely agree as long as we’re talking about APIs that you are building. In other words, always return a Task (or IAsnycAction/Operation) if you have control over the signature of the method. The button click handler example above however is an example of one you don’t have control over and therefore can’t avoid on the other hand. Even if you never intend to await the task (e.g. you are building a method to be called from a constructor only), you should still return the task. Later you may want to call it from another place that you can await from or you may want to store the task object in a variable and await it later.

Async events

Awaiting an event handler is not something the language supports out of the box. All you can do is raise an event (without await) and carry on. However, there is a great pattern called “deferrables” that you can use in your own events to allow the code that raised the event to wait for an async operation that occurs in an event handler. To do this you need to create a custom EventArgs class that allows the event handler to request a deferral, perform its async operation and then signal back to the code that raised the event that it is complete. The code that raised the event can then await the collection of deferrals requested by one or more event handlers before continuing.

For a more detailed explanation and code samples, see my Custom async events in C# blog post.

Async properties

Async getters and setters are also not possible in C#. However, there are some tricks you can use to work around this as well. One approach is to simply get rid of the property and use Get and Set methods instead. However, this makes binding to a UI difficult. Another way that allows you to use the binding power of properties is to use the power of INotifiyPropertyChanged to your advantage.

For example, in a getter you could do something like this:

int? highScore; public int HighScore { get { if (highScore.HasValue) return highScore.Value; else { GetHighScoreAsync(CancellationToken.None).ContinueWith(t => { highScore = t.Result; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HighScore))); }, TaskScheduler.FromCurrentSynchronizationContext()); return default(int); } } }

In the case of a setter you can simply call an async method to set your value without awaiting the result.

In both cases, just be cognoscente about re-entrant scenarios where the getter or setter is called more than once before the internal async call returns. To help with this you can build a task queue in the case of the setter or store the active task in the case of a getter and do a null check first before continuing. For example:

int? highScore; Task highScoreTask; public int HighScore { get { if (highScore.HasValue) return highScore.Value; else if (highScoreTask == null) { highScoreTask = UpdateHighScoreAsync(CancellationToken.None); } return default(int); } } async Task UpdateHighScoreAsync(CancellationToken cancellationToken) { try { highScore = await GetHighScoreAsync(cancellationToken); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HighScore))); } finally { highScoreTask = null; } }

Other resources

https://blogs.msdn.microsoft.com/andrewarnottms/2014/03/19/recommended-patterns-for-cancellationtoken/

https://programmerpayback.com/2015/09/03/custom-async-events-in-c/