This little “warning” has been the bain of my life recently…
warning CS1998: This async method lacks ‘await’ operators and will run synchronously. Consider using the ‘await’ operator to await non-blocking API calls, or ‘await Task.Run(…)’ to do CPU-bound work on a background thread.
And after a bit of searching around, I just wanted to touch on it because I think it’s a fairly common warning in C# code, and one that can either point to imminent danger, or be absolutely meaningless depending on your code. And it seems like there are all sorts of hokey “fixes” that don’t address why this happens in the first place.
Why/Where This Happens
The most common place I’ve seen code like this is where I’ve had to implement an interface that expects that your method likely needs to be async, but you don’t actually need anything async.
As an example, I had an interface for “validating” accounts. Something like so :
public interface IAccountValidator { Task Validate(Account account); }
But one of my implementations for this code was simply checking if the username was “admin” or not.
public class AccountValidatorRestrictedAdmin : IAccountValidator { public async Task Validate(Account account) { if (account.Username == "admin") throw new Exception("Unable to use username admin"); } }
Now this particular piece of code is not async and does not contain async code. But other validators *are* async. So I run into a warning for this method. Oof. There isn’t really a great way to avoid this sort of implementation because
- Creating non-async code upfront is likely to come back and bite me later if I actually do need to be async. Since we try and preach “async all the way down”, to then break this will be a pain.
- I don’t want to have two different methods/interfaces, one async and the other sync to do the same thing.
Now actually, this warning can be “ignored” (Well.. Kinda) in most cases…..
The Imminent Danger Code
Before we get into the talk of where this warning isn’t a problem, let’s talk about the warning where your code is going to blow up.
Suppose I have the following method :
public async Task Validate(Account account) { CheckSomethingAsync(); }
Where CheckSomethingAync() truly is async. Then I’m going to get the same warning again, but I’m also going to get another.
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.
These often come in two’s because one is saying that your method is async, but doesn’t await anything. But the second one tells you exactly why that is the case, because you’re actually *calling* an async method, but not awaiting it. This is generally speaking, a warning that *cannot* be ignored.
I’ll repeat, if you get warning CS4014 saying that an actual call is not awaited, in almost all cases your code requires an await somewhere and you should fix it!
The Overheads of Async/Await
Now if you’ve made it this far, and your code isn’t in the danger zone, we can talk about why exactly you get the warning on an async method running synchronously. After all, if a method is marked as async, and it runs sync, is there really a problem?
I really don’t profess to be an expert in all things async. In fact, all I really know on the subject is from a pluralsight course from Jon Skeet dating back to ~2016 (Which by the way, has since been taken down which is a great shame because it was incredible!). But from what I can tell, there is no actual issue with a method marked as async, that does not call async methods.
The only thing that I could find is that when you have a method like so :
public async Task Validate(Account account) { if (account.Username == "admin") throw new Exception("Unable to use username admin"); }
There is an overhead in creating the state machine for an asynchronous method that won’t ever be used. I really couldn’t find much by way of measuring this overhead, but I think it’s relatively safe to say that it’s minimal as it would be the same overhead if your method was actually async.
So I feel nervous coming to this conclusion but generally speaking, other than the annoying warning, I can’t find anything dangerous about this code and it seems to me to be relatively safe to ignore (As long as you don’t have the second error labelled above!)
Using Task.FromResult/Task.CompletedTask
Many guides on this warning point to the usage of Task.FromResult to hide it. For example :
public Task Validate(Account account) { if (account.Username == "admin") throw new Exception("Unable to use username admin"); return Task.CompletedTask; }
Instead of the method being async, it still returns a Task (To conform with an interface as an example), but instead returns Task.CompletedTask. If you have to return a result, then returning Task.FromResult() is also a valid way to do this.
This works but, to me it makes the code look bizarre. Returning empty tasks or values wrapped in tasks implies that my code is a lot more complicated than it actually is. Just in my opinion of course, but it is a suitable way of getting rid of the warning.
Suppressing Warnings
This is going to see my be eaten alive on Twitter I’m sure, but you can also supress the warnings in your project if you are sure that they are of no help to you.
To do so, edit your csproj file and add the following :
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <NoWarn>1998</NoWarn> </PropertyGroup> ... </Project>
Where 1998 is the original warning code. Again, *do not add 4014* as this, in almost all cases, is an example of code that’s about to blow up in your face.
Do Not Task.Yield()
Finally, the absolute worst thing to do is to add a “do nothing” Task.Yield().
public async Task Validate(Account account) { if (account.Username == "admin") throw new Exception("Unable to use username admin"); await Task.Yield(); }
This also gets rid of the warning, but is incredibly bad for performance. You see, when you call await, in some cases it may not need to “juggle” the process at all. It may just be able to continue along the execution path. However when you call Task.Yield(), you actually force the code to await. It’s no longer an “option”.
If that doesn’t make too much sense, that’s OK, it doesn’t really make much sense when I say it out loud either. But just know that adding a Task.Yield() for the sole purpose of bypassing a warning in your code is up there in terms of the worst things you can do.
I have an annoying 1998 warning in my code, only for some setup-code in a test – the real code is always async (loading from a data store) – but the point of the test is to just set some fixed values, as such not async.
Reading this article, I decided not to do the await Task.Yield() (even though the performance of this test is not particularly important).
But I don’t want to disable the warning for all tests, so with a little bit of googling I found the compiler directive for the suggested solution:
#pragma warning disable 1998
#pragma warning restore 1998
I find that a bit better than just disabling the warning for the whole project.
Actually, there is one subtlety, that may, or may not, be a problem. Consider this code
async Task Validate1() => throw new Exception(“Invalid1”);
Task Validate2() => throw new Exception(“Invalid1”);
await Task.WhenAll(Validate1(), Validate2());
One might assume that the Task.WhenAll will await both tasks and then throw. But in fact it will neve be invoked since Validate2 will throw instead of returning a Task. The exception Task from Validate1 will now not be awaited and will eventually trigger an UnobservedTaskException and the validation will only see the Validate2 exception.