Finding Async Method Calls Missing Await

I’ve run into this issue not only when migrating legacy projects to use async/await in C# .NET, but even just day to day on greenfields projects. The issue I’m talking about involves code that looks like so :

static async Task Main(string[] args)
{
    MyAsyncMethod(); // Oops I forgot to await this!
}

static async Task MyAsyncMethod()
{
    await Task.Yield();
}

It can actually be much harder to diagnose than you may think. Due to the way async/await works in C#, your async method may not *always* be awaited. If the async method completes before it has a chance to wait, then your code will actually work much the same as you expect. I have had this happen often in development scenarios, only for things to break only in test. And the excuse of “but it worked on my machine” just doesn’t cut it anymore!

In recent versions of .NET and Visual Studio, there is now a warning that will show to tell you your async method is not awaited. It gives off the trademark green squiggle :

And you’ll receive a build warning with the text :

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.

The problem with this is that the warning isn’t always immediately noticeable. On top of this, a junior developer may not take heed of the warning anyway.

What I prefer to do is add a line to my csproj that looks like so :

<PropertyGroup>
    <WarningsAsErrors>CS4014;</WarningsAsErrors>
</PropertyGroup>

This means that every async method that is not awaited will actually stop the build entirely.

Disabling Errors By Line

But what if it’s one of those rare times you actually do want to fire and forget (Typically for desktop or console applications), but now you’ve just set up everything to blow up? Worse still the error will show if you are inside an async method calling a method that returns a Task, even if the called method is not itself async.

But we can disable this on a line by line basis like so :

static async Task Main(string[] args)
{
    #pragma warning disable CS4014 
    MyAsyncMethod(); // I don't want to wait this for whatever reason, it's not even async!
    #pragma warning restore CS4014
}

static Task MyAsyncMethod()
{
    return Task.CompletedTask;
}

Non-Awaited Tasks With Results

Finally, the one thing I have not found a way around is like so :

static async Task Main(string[] args)
{
    var result = MyAsyncMethodWithResult();
    var newResult = result + 10;//Error because result is actually an integer. 
}

static async Task<int> MyAsyncMethodWithResult()
{
    await Task.Yield();
    return 0;
}

This code will actually blow up. The reason being that we expect the value of result to be an integer, but in this case because we did not await the method, it’s a task. But what if we pass the result to a method that doesn’t care about the type like so :

static async Task Main(string[] args)
{
    var result = MyAsyncMethodWithResult();
    DoSomethingWithAnObject(result);
}

static async Task MyAsyncMethodWithResult()
{
    await Task.Yield();
    return 0;
}

static void DoSomethingWithAnObject(object myObj)
{
}

This will not cause any compiler warnings or errors (But it will cause runtime errors depending on what DoSomethingWithAnObject does with the value).

Essentially, I found that the warning/error for non awaited tasks is not shown if you assign the value to a variable. This is even the case with Tasks that don’t return a result like so :

static async Task Main(string[] args)
{
    var result = MyAsyncMethod(); // No error
}

static async Task MyAsyncMethod()
{
    await Task.Yield();
}

I have searched high and low for a solution for this but most of the time it leads me to stack overflow answers that go along the lines of “Well, if you assigned the value you MIGHT actually want the Task as a fire and forget”. Which I agree with, but 9 times out of 10, is not going to be the case.

That being said, turning the compiler warnings to errors will catch most of the errors in your code, and the type check system should catch 99% of the rest. For everything else… “Well it worked on my machine”.

7 thoughts on “Finding Async Method Calls Missing Await”

  1. There’s a reason you aren’t supposed to force await. Otherwise it wouldn’t be optional. Await is a tool to be used when appropriate. You aren’t supposed to always await every time you write asynchronous code. The implication is that you are supposed to understand the mechanics of asynchronous programming and await only when it is appropriate for the task at hand. Just because you don’t use await doesn’t mean your code is bound to blow up. Having your ide error out just because you don’t use await is 100% an anti pattern that simply teaches you an incorrect understanding of the concepts you are implementing. I recommend spending some time exploring asyncronous programming concepts before working so hard at correcting your IDE. In my opinion your default IDE warnings are a little over the top. Those warnings are not there to tell you that something is wrong, but rather to draw your attention to the fact that it can’t tell if something is wrong or not so that you can double check your own work. The decision of whether you should await is entirely contextual and is a matter of programmer judgement

    Reply
    • I’m honestly going to have to disagree. In business programming sense, for example a CRUD API, under what circumstance would you *not* be awaiting every async method by default, and instead using the task in some way? I get that you can, but it’s more of an outlier these days. Maybe if you given an example of a popular C# library (In the vein of something like Automapper), or something in the .NET Framework that uses async/await heavily (For example .NET Pipelines/Channels), that uses async/await in the way that you envisage it to be used?

      And just to be clear, we are talking about asynchronous programming, not multi threaded programming.

      Reply
  2. One solution would be to get into the habit of only using “var” when the right-hand side of the assignment is either a constructor call or a literal:

    var x = 5;
    var person = new Person(“Joe”, “Schmoe”);
    int result = MyAsyncMethod(person, x); // Error, because the method does not return an int

    Reply
  3. Very interesting post and useful to add the on every projects.

    I also worked many years on greenfields project and now on a legacy one (asp.net classic) and it’s even more useful in legacy project where inherent complexity of the project can easily hide these mistakes. It’s en easy way to enforce best pratices with task.

    I can’t see good reason for letting task unawaited. If a real life issue would appear, I would also use #pragma warning disable CS4014 and explain that unexpected case to next developer.

    Reply

Leave a Comment