DEV Community

Cover image for Common Async/Await mistakes in C#
Junior Morais
Junior Morais

Posted on

Common Async/Await mistakes in C#

Hello, developers! šŸ„·šŸ»

Today, we are going to talk about common mistake made when using asynchronous programming in C# projects.

First of all, let’s understand what is asynchronous programming and why it is so powerful.

Let’s imagine we have the following 4 tasks:

  1. Task A;
  2. Task B;
  3. Task C;
  4. Task D;

Each of these tasks takes 4 seconds to complete. So, running this code as synchronous, it will take 16 seconds to finish all tasks. In synchronous programming, the code runs sequentially. Task D will run when all those 3 (A, B and C) have finished. In this scenario, the thread will be blocked until the task is finished.

using System;
using System.Threading;

class Program
{
     static void Main(string[] args)
    {
        RunSynchronously();
    }

    static void Task(string name)
    {
        Console.WriteLine($"Starting {name}...");
        Thread.Sleep(4000);
        Console.WriteLine($"{name} finished.");
    }

    static void RunSynchronously()
    {
        Console.WriteLine("Running tasks synchronously...");
        Task("Task A");
        Task("Task B");
        Task("Task C");
        Task("Task D");
        Console.WriteLine("All tasks completed synchronously.");
    }
}
Enter fullscreen mode Exit fullscreen mode
Running tasks synchronously...
Starting Task A...
Task A finished.
Starting Task B...
Task B finished.
Starting Task C...
Task C finished.
Starting Task D...
Task D finished.
All tasks completed synchronously.
Enter fullscreen mode Exit fullscreen mode

Now, running the same code asynchronously, we can finish all tasks in approximately 5 seconds instead of 16. Why is that? The answer is simple: in this case, there are no blocked threads, and every task can run independently.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task TaskAsync(string name)
    {
        Console.WriteLine($"Starting {name}...");
        await Task.Delay(4000);
        Console.WriteLine($"{name} finished.");
    }

    static async Task RunAsynchronously()
    {
        Console.WriteLine("Running tasks asynchronously...");
        await Task.WhenAll(
            TaskAsync("Task A"),
            TaskAsync("Task B"),
            TaskAsync("Task C"),
            TaskAsync("Task D")
        );
        Console.WriteLine("All tasks completed asynchronously.");
    }
}
Enter fullscreen mode Exit fullscreen mode
Running tasks asynchronously...
Starting Task A...
Starting Task B...
Starting Task C...
Starting Task D...
Task A finished.
Task B finished.
Task C finished.
Task D finished.
All tasks completed asynchronously.
Enter fullscreen mode Exit fullscreen mode

Since we have the general understating of how async programming works, lets dive into common mistakes using async programming.


Avoid async void methods

In C#, async void methods are generally considered dangerous and should be avoided in most cases, and the reasons of this are:

  1. async void methods cannot be not awaited**: When you use async void, the method does not return a Task or Task. This means the caller cannot await it or know when it has completed. This makes it impossible to handle exceptions or control the flow of execution properly. As the consequence, this unhandled exception has the potential to crash the application.
async void DangerousMethod()
{
    await Task.Delay(1000);
    throw new Exception("Unhandled exception!");
}

void Caller()
{
    DangerousMethod(); // Exception will crash the app
}
Enter fullscreen mode Exit fullscreen mode
async Task SafeMethod()
{
    await Task.Delay(1000);
    throw new Exception("Something went wrong!");
}

async Task Caller()
{
    try
    {
        await SafeMethod(); // Exception can be caught here
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Caught exception: {ex.Message}");
    }
}
Enter fullscreen mode Exit fullscreen mode

There are very few scenarios where async void is acceptable, for example:

  1. Event Handlers: Event handlers are inherently void, so async void is the only option (e.g., Button.Click in WPF or WinUI3).
private async void Button_Click(object sender, EventArgs e)
{
    try
    {
        await Task.Delay(1000);
        throw new Exception("Something went wrong!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Caught exception: {ex.Message}");
    }
}
Enter fullscreen mode Exit fullscreen mode

AvoidĀ .Wait()Ā andĀ .ResultĀ whenever possible

TheĀ .Wait()Ā method andĀ .ResultĀ property in .NET are blocking calls that wait for aĀ TaskĀ to complete. While they may seem straightforward, they have significant downsides, particularly in certain contexts:

  1. On the Main Thread: UsingĀ .Wait()Ā orĀ .ResultĀ on the main thread can freeze the UI. This happens because both methods block the thread until the task completes, preventing the UI thread from processing other events.
  2. Deadlocks: UsingĀ .Wait()Ā orĀ .ResultĀ inappropriately can lead to deadlocks, especially in applications with aĀ synchronization contextĀ (e.g., UI applications). A deadlock occurs when the blocked thread is waiting for a task to complete, but the task itself is trying to post back to the same thread to finish execution. Since the thread is blocked, the task cannot complete, resulting in a deadlock.
Task.Run(() => DoWork()).Wait(); 
Enter fullscreen mode Exit fullscreen mode
await Task.Run(() => DoWork()); 
Enter fullscreen mode Exit fullscreen mode

Key Differences BetweenĀ .ResultĀ andĀ .Wait()

Feature .Result .Wait()
Return Value Returns the result of theĀ Task. Does not return a value.
Blocking Blocks until the task completes. Blocks until the task completes.
Deadlock Risk High in synchronization contexts. High in synchronization contexts.

PreferĀ .GetAwaiter().GetResult()Ā overĀ .ResultĀ orĀ .Wait()

Using .GetAwaiter().GetResult() is sometimes recommended as an alternative to .Result or .Wait() because it avoids wrapping exceptions in an AggregateException.

When you use .Result or .Wait() on a task that throws an exception, the exception is wrapped in an AggregateException. This makes it harder to handle the actual exception because you need to unwrap it using InnerException.

try
{
    var result = SomeAsyncMethod().GetAwaiter().GetResult();
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
Enter fullscreen mode Exit fullscreen mode

Try to use .ConfigureAwait(false) in async methods whenever is possible

By default, when youĀ awaitĀ a task in an asynchronous method, the continuation (the code after theĀ await) tries to resume execution on theĀ original synchronization contextĀ orĀ threadĀ where theĀ awaitĀ was called. This is particularly important in UI frameworks like WPF or WinUI3, where updates to the UI must happen on the main thread!

When you useĀ ConfigureAwait(false), you tell the runtimeĀ not to capture the synchronization context. This means the continuation can run on any available thread, rather than being forced to return to the original thread.

When not to useĀ .ConfigureAwait(false)

Avoid usingĀ .ConfigureAwait(false)Ā in methods that need to update the UI. The continuation must run on the main thread to safely interact with UI elements.

Remember, the asynchronous task itself runs independently of the main thread. However, the continuation after theĀ awaitĀ may return to the main thread!

public async Task UpdateUIAsync()
{
    await Task.Delay(1000);
    MyLabel.Text = "Updated!";
}
Enter fullscreen mode Exit fullscreen mode

CancellationTokenshould be used in all async methods

AĀ CancellationTokenĀ is a mechanism in .NET that allows you to signal and handle the cancellation of an asynchronous operation. It is part of theĀ System.ThreadingĀ namespace and is commonly used inĀ asyncĀ methods to provide a way for the caller to cancel the operation if it is no longer needed. It allows you to gracefully cancel long-running or resource-intensive operations.

Best practices using CancellationToken

Always include aĀ CancellationToken

  • Add aĀ CancellationTokenĀ parameter to allĀ asyncĀ methods, even if you don't currently use it.
  • If your method doesn't require cancellation, you can useĀ CancellationToken.NoneĀ as a default value.

Check for cancellation regularly

For long-running loops or operations, periodically check:

  • cancellationToken.IsCancellationRequestedĀ 

  • Or callĀ cancellationToken.ThrowIfCancellationRequested()

Avoid swallowing cancellation exceptions

  • Do not catch and ignoreĀ OperationCanceledException.
  • Always handle it appropriately to ensure the operation is canceled cleanly.

Conclusion!

Asynchronous programming in C# is a powerful tool that can significantly improve the performance and responsiveness of your applications. However, it comes with its own set of challenges and pitfalls. By understanding and following these best practices, you can write more efficient, maintainable, and error-free asynchronous code.

Happy coding! šŸ‘ØšŸ»ā€šŸ’»

Top comments (0)