Fixing the Sync over Async Antipattern in C#

By FoxLearn 1/21/2025 7:51:41 AM   14
One common mistake developers make is the Sync over Async antipattern, which occurs when blocking waits are used in asynchronous methods instead of awaiting them properly.

The Sync over Async antipattern happens when blocking calls are used, such as Wait() or Result, on asynchronous methods. These methods are designed to be non-blocking, but when developers use blocking calls, they end up blocking the thread until the task completes, which can be detrimental to both performance and responsiveness, especially in user interface (UI) applications.

The two most common causes of this antipattern are:

  1. Calling Wait() on the Task returned by an asynchronous method.
  2. Using Task.Result, which also causes a blocking wait.

This article explores an example of the Sync over Async antipattern and provides a solution to fix it by converting the blocking calls into proper asynchronous code using await.

To illustrate this issue, let's consider a simple weather application that fetches weather data from an API asynchronously using HttpClient. However, the current implementation incorrectly blocks the thread with Wait() and Result calls.

public partial class frmWeather : Form
{        
    private readonly string API_KEY = "<blanked out>";
    private readonly HttpClient httpClient = new HttpClient();

    public frmWeather()
    {
        InitializeComponent();
    }

    private void btnGetWeather_Click(object sender, EventArgs e)
    {
        txtWeather.Text = GetWeatherData(txtCity.Text);
    }

    public string GetWeatherData(string City)
    {
        var url = $"http://api.openweathermap.org/data/2.5/weather?q={City}&units=imperial&APPID={API_KEY}";

        var responseTask = httpClient.GetAsync(url);
        responseTask.Wait(); // Blocking call
        responseTask.Result.EnsureSuccessStatusCode();

        var contentTask = responseTask.Result.Content.ReadAsStringAsync();
        string responseData = contentTask.Result; // Another blocking call
        return responseData;
    }
}

In this code:

  • responseTask.Wait() blocks the calling thread until the HTTP request completes.
  • contentTask.Result also blocks until the content is read from the HTTP response.

These blocking calls can lead to UI freezing and poor performance.

Fixing the Sync over Async Antipattern

To fix this, we need to convert the method GetWeatherData into an asynchronous method and use await instead of blocking calls.

Convert GetWeatherData to Async

The first step is to change the GetWeatherData method to return a Task<string> and make it asynchronous. This allows us to use await within the method.

public async Task<string> GetWeatherData(string City)
{
    var url = $"http://api.openweathermap.org/data/2.5/weather?q={City}&units=imperial&APPID={API_KEY}";

    var httpResponse = await httpClient.GetAsync(url); // Use await
    httpResponse.EnsureSuccessStatusCode();

    return await httpResponse.Content.ReadAsStringAsync(); // Use await
}

Update the Caller to Use Async/Await

The event handler btnGetWeather_Click also needs to be updated to use await. Note that in UI event handlers, it is acceptable to use async void.

private async void btnGetWeather_Click(object sender, EventArgs e)
{
    txtWeather.Text = await GetWeatherData(txtCity.Text); // Use await
}

Remove Blocking Waits

Now that the method is asynchronous, we can remove the blocking calls to Wait() and Result. By awaiting the tasks, we ensure that the thread is not blocked, and the method returns a result once the task is complete.

Here is the final, corrected version of the code after fixing the Sync over Async antipattern:

private async void btnGetWeather_Click(object sender, EventArgs e)
{
    txtWeather.Text = await GetWeatherData(txtCity.Text); // Use await
}

public async Task<string> GetWeatherData(string City)
{
    var url = $"http://api.openweathermap.org/data/2.5/weather?q={City}&units=imperial&APPID={API_KEY}";

    var httpResponse = await httpClient.GetAsync(url); // Use await
    httpResponse.EnsureSuccessStatusCode();

    return await httpResponse.Content.ReadAsStringAsync(); // Use await
}

By using async and await, the code is more efficient and responsive:

  • No blocking: The main thread is not blocked waiting for the task to complete.
  • Simpler code: Using await eliminates the need for Wait() and Result, simplifying the code.
  • Better performance: This ensures that your app does not freeze or become unresponsive, especially in UI-based applications.

The Sync over Async antipattern is a common issue in asynchronous programming, but it’s easily fixable by using await instead of Wait() and Result.