Skip to content

async await

The following chapter will deep dive into tips, tricks and pitfalls when it comes down to async and await in C#.

Elide await keyword - Exceptions

Eliding the await keyword can lead to a less traceable stacktrace due to the fact that every Task which doesn’t get awaited, will not be part of the stack trace.

Bad

using System;
using System.Threading.Tasks;

try
{
    await DoWorkWithoutAwaitAsync();
}
catch (Exception e)
{
    Console.WriteLine(e);
}

static Task DoWorkWithoutAwaitAsync()
{
    return ThrowExceptionAsync();
}

static async Task ThrowExceptionAsync()
{
    await Task.Yield();
    throw new Exception("Hey");
}

Will result in

System.Exception: Hey  
 at Program.<<Main>$>g__ThrowExceptionAsync|0_1()  
 at Program.<Main>$(String[] args)  

Good If speed and allocation is not very crucial, add the await keyword.

using System;
using System.Threading.Tasks;

try
{
    await DoWorkWithoutAwaitAsync();
}
catch (Exception e)
{
    Console.WriteLine(e);
}

static async Task DoWorkWithoutAwaitAsync()
{
    await ThrowExceptionAsync();
}

static async Task ThrowExceptionAsync()
{
    await Task.Yield();
    throw new Exception("Hey");
}

Will result in:

System.Exception: Hey
   at Program.<<Main>$>g__ThrowExceptionAsync|0_1()
   at Program.<<Main>$>g__DoWorkWithoutAwaitAsync|0_0()
   at Program.<Main>$(String[] args)

💡 Info: Eliding the async keyword will also elide the whole state machine. In very hot paths that might be worth a consideration. In normal cases one should not elide the keyword. The allocations one is saving is depending on the circumstances but a normally very very small especially if only smaller objects are passed around. Also performance-wise there is no big gain when eliding the keyword (we are talking nano seconds). Please measure first and act afterwards.

Elide await keyword - using block

Eliding inside an using block can lead to a disposed object before the Task is finished.

Bad Here the download will be aborted / the HttpClient gets disposed:

public Task<string> GetContentFromUrlAsync(string url)
{
    using var client = new HttpClient();
        return client.GetStringAsync(url);
}

Good

public async Task<string> GetContentFromUrlAsync(string url)
{
    using var client = new HttpClient();
        return await client.GetStringAsync(url);
}

💡 Info: Eliding the async keyword will also elide the whole state machine. In very hot paths that might be worth a consideration. In normal cases one should not elide the keyword. The allocations one is saving is depending on the circumstances but a normally very very small especially if only smaller objects are passed around. Also performance-wise there is no big gain when eliding the keyword (we are talking nano seconds). Please measure first and act afterwards.

Return null Task or Task<T>

When returning directly null from a synchronous call (no async or await) will lead to NullReferenceException:

Bad Will throw NullReferenceException

await GetAsync();

static Task<string> GetAsync()
{
    return null;
}

Good Use Task.FromResult:

await GetAsync();

static Task<string> GetAsync()
{
    return Task.FromResult(null);
}

async void

The problem with async void is first they are not awaitable and second they suffer the same problem with exceptions and stack trace as discussed a bit earlier. It is basically fire and forget.

Bad Not awaited

public async void DoAsync()
{
    await SomeAsyncOp();
}

Good return Task instead of void

public async Task DoAsync()
{
    await SomeAsyncOp();
}

💡 Info: There are valid cases for async void like top level event handlers.

List<T>.ForEach with async

List<T>.ForEach and in general a lot of LINQ methods don’t go well with async await:

Bad Is the same as async void

var ids = new List<int>();
// ...
ids.ForEach(id => _myRepo.UpdateAsync(id));

One could thing adding async into the lamdba would do the trick:

Bad Still the same as async void because List<T>.ForEach takes an Action and not a Func<Task>.

var ids = new List<int>();
// ...
ids.ForEach(async id => await _myRepo.UpdateAsync(id));

Good Enumerate through the list via foreach

foreach (var id in ids)
{
    await _myRepo.UpdateAsync(id);
}

Favor await over synchronous calls

Using blocking calls instead of await can lead to potential deadlocks and other side effects like a poor stack trace in case of an exception and less scalability in web frameworks like ASP.NET core.

Bad This call blocks the thread.

public async Task SomeOperationAsync()
{
    await ...
}

public void Do()
{
    SomeOperationAsync().Wait();
}

Good Use async & await in the whole chain

public async Task SomeOperationAsync()
{
    await ...
}

public async Task Do()
{
    await SomeOperationAsync();
}

Favor GetAwaiter().GetResult() over Wait and Result

Task.GetAwaiter().GetResult() is preferred over Task.Wait and Task.Result because it propagates exceptions rather than wrapping them in an AggregateException.

Bad

string content = DownloadAsync().Result;

Good

string content = DownloadAsync().GetAwaiter().GetResult();

Don’t use Task.Delay for small precise waiting times

Task.Delay‘s internal timer is dependent on the underlying OS. On most windows machines this resolution is about 15ms.

So: Task.Delay(1) will not wait one millisecond but something between one and 15 milliseconds.

var stopwatch = Stopwatch.StartNew();
await Task.Delay(1);
stopwatch.Stop(); // Don't account the Console.WriteLine into the timer
Console.WriteLine($"Delay was {stopwatch.ElapsedMilliseconds} ms");

Will print for example:

Delay was 6 ms

Properly awaiting concurrent tasks

Often times tasks are independent of each other and can be awaited independently.

Bad The following code will run roughly 1 second.

await DoOperationAsync();
await DoOperationAsync();

async Task DoOperationAsync()
{
    await Task.Delay(500);
} 

Good When tasks or their data is independent they can be awaited independently for maximum benefits. The following code will run roughly 0.5 seconds.

var t1 = DoOperationAsync();
var t2 = DoOperationAsync();
await t1;
await t2;

async Task DoOperationAsync()
{
    await Task.Delay(500);
} 

An alternative to this would be Task.WhenAll:

var t1 = DoOperationAsync();
var t2 = DoOperationAsync();
await Task.WhenAll(t1, t2); // Can also be inlined

async Task DoOperationAsync()
{
    await Task.Delay(500);
} 

ConfigureAwait with await using statement

Since C# 8 you can provide an IAsyncDisposable which allows to have asynchrnous code in the Dispose method. Also this allows to call the following construct:

await using var dbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);

In this example CreateDbContextAsync uses the ConfigureAwait(false) but not the IAsyncDisposable. To make that work we have to break apart the statment like this:

var dbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
    // ...
}

The last part has the “ugly” snippet that you have to introduce a new “block” for the using statement. For that there is a easy workaround:

var blogDbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
await using var _ = blogDbContext.ConfigureAwait(false);
// You don't need the {} block here

Avoid Async in Constructors

Constructors are meant to initialize objects synchronously, as their primary purpose is to set up the initial state of the object. When you need to perform asynchronous operations during object initialization, using async methods in constructors can lead to issues such as deadlocks or incorrect object initialization. Instead, use a static asynchronous factory method or a separate asynchronous initialization method to ensure safe and proper object initialization.

Using async operations in constructors can be problematic for several reasons like Deadlocks, Incomplete Initialization, and Exception Handling.

Bad Calling async methods in constructors

public class MyClass
{
    public MyClass()
    {
        InitializeAsync().GetAwaiter().GetResult();
    }

    private async Task InitializeAsync() => await LoadDataAsync();

Good: Option 1 - Static asynchronous factory method:

public class MyClass
{
    private MyClass() { }

    public static async Task<MyClass> CreateAsync()
    {
        var instance = new MyClass();
        await instance.InitializeAsync();
        return instance;
    }

    private async Task InitializeAsync() => await LoadDataAsync();

Good: Option 2 - Separate asynchronous initialization method:

public class MyClass
{
    public async Task InitializeAsync() => await LoadDataAsync();