DEV Community

Cover image for 5 async/await traps in C# that compile fine
Naze Code
Naze Code

Posted on

5 async/await traps in C# that compile fine

async/await makes asynchronous code look synchronous. That's the point, and it's also the problem: these five snippets look correct, build fine, and go wrong at runtime. Every output below is copied from actually running them on .NET 10.

1. The missing await

public static async Task CheckoutAsync(Order order)
{
    await ChargeAsync(order);
    SendReceiptAsync(order);
    Console.WriteLine("Checkout complete");
}
Enter fullscreen mode Exit fullscreen mode

In the demo, the email server is down, so SendReceiptAsync throws:

Sending receipt...
Checkout complete
Enter fullscreen mode Exit fullscreen mode

That's all. The exception happens inside a task nobody awaits, so nobody ever hears about it. The customer is charged and never gets a receipt.

The compiler does warn you:

warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
Enter fullscreen mode Exit fullscreen mode

Fix: await it. With await, the failure reaches the caller:

Sending receipt...
System.InvalidOperationException: Email server unavailable
Enter fullscreen mode Exit fullscreen mode

Better still, make that warning an error in your .csproj so it can't be missed:

<WarningsAsErrors>CS4014</WarningsAsErrors>
Enter fullscreen mode Exit fullscreen mode

2. async void and the try/catch that never catches

public static void OnTimer()
{
    try { RefreshCache(); }
    catch (Exception e) { Log(e); }
}

static async void RefreshCache()
{
    await Task.Delay(100);
    throw new HttpRequestException("API down");
}
Enter fullscreen mode Exit fullscreen mode
Unhandled exception. System.Net.Http.HttpRequestException: API down
Enter fullscreen mode Exit fullscreen mode

The whole process crashes. async void returns nothing the caller can wait on, so RefreshCache() returns at the first await and the try block is already over when the exception happens.

Fix: async Task, and await it.

public static async Task OnTimerAsync()
{
    try { await RefreshCacheAsync(); }
    catch (Exception e) { Log(e); }
}
Enter fullscreen mode Exit fullscreen mode
Logged: API down
Enter fullscreen mode Exit fullscreen mode

The one legitimate use of async void is a top-level event handler, such as a button click.

3. List.ForEach with an async lambda

var orders = await LoadOrdersAsync();

orders.ForEach(async o => await SaveAsync(o));

Console.WriteLine($"Saved {orders.Count} orders");
Enter fullscreen mode Exit fullscreen mode
Saved 3 orders
Enter fullscreen mode Exit fullscreen mode

SaveAsync prints saved order #n each time it finishes. None of those lines appear. The program reported success and exited before a single order was saved.

ForEach takes an Action, so the async lambda turns into async void: trap 2 again, hidden inside a one-liner.

Fix: a plain foreach with await.

foreach (var o in orders)
    await SaveAsync(o);
Enter fullscreen mode Exit fullscreen mode
saved order #1
saved order #2
saved order #3
Saved 3 orders
Enter fullscreen mode Exit fullscreen mode

4. .Result on a UI thread

// Button click handler, runs on the UI thread
public static void OnLoadClicked()
{
    var name = LoadUserNameAsync().Result;
    Show(name);
}
Enter fullscreen mode Exit fullscreen mode

The demo runs this on a single-threaded context that behaves like a WinForms/WPF UI thread, with a 3-second watchdog:

[watchdog] no response after 3.0 s -> killed (deadlock)
Enter fullscreen mode Exit fullscreen mode

.Result blocks the UI thread while it waits. The await inside LoadUserNameAsync wants to finish on that same UI thread. Each waits for the other forever, and the app freezes.

The same code in a plain console app works:

Label: Ada
Enter fullscreen mode Exit fullscreen mode

That's why this one survives testing. Console apps and ASP.NET Core don't have that single-threaded context, so the classic deadlock only shows up in UI apps (WinForms, WPF, MAUI) and old ASP.NET.

Fix: await all the way up.

// Event handlers are the one place async void is OK
public static async void OnLoadClicked()
{
    var name = await LoadUserNameAsync();
    Show(name);
}
Enter fullscreen mode Exit fullscreen mode
Label: Ada
Enter fullscreen mode Exit fullscreen mode

5. Waiting for things one at a time

Three independent calls, about 1 second each:

var user    = await GetUserAsync(id);
var orders  = await GetOrdersAsync(id);
var reviews = await GetReviewsAsync(id);
Enter fullscreen mode Exit fullscreen mode
one by one: 3.03 s
Enter fullscreen mode Exit fullscreen mode

Each await waits for the previous call to finish before starting the next.

Fix: start them all, then wait once.

var userTask    = GetUserAsync(id);
var ordersTask  = GetOrdersAsync(id);
var reviewsTask = GetReviewsAsync(id);

await Task.WhenAll(userTask, ordersTask, reviewsTask);

var user    = await userTask;    // already finished
var orders  = await ordersTask;
var reviews = await reviewsTask;
Enter fullscreen mode Exit fullscreen mode
WhenAll:    1.02 s
Enter fullscreen mode Exit fullscreen mode

Only do this for independent work. For example, EF Core doesn't allow parallel operations on the same DbContext.

Recap

  1. Await every task you start, and make CS4014 an error.
  2. async Task, never async void (except top-level event handlers).
  3. No ForEach(async …). Use foreach + await.
  4. Don't block on async code with .Result or .Wait(). Await all the way up.
  5. Independent calls: start them together and await Task.WhenAll.

Which async bug took you the longest to track down? Tell me in the comments.

I make short, tested videos about C# bugs that compile fine and still break things. More on the Naze Code YouTube channel.


Sources: Async/Await best practices (MSDN Magazine), ConfigureAwait FAQ (.NET Blog), ASP.NET Core SynchronizationContext (Stephen Cleary), DbContext configuration (EF Core docs). Tested on .NET SDK 10.0.302.

Top comments (0)