async
in C#I recently changed a controller action’s method signature like so:
[HttpGet("{id}")]
- public Task async GetRecordAsync(int id) => await _recordRepository.GetAsync(id);
+ public Task GetRecordAsync(int id) => _recordRepository.GetAsync(id);
During code review, my team members quickly spotted this odd change. They questioned the removal of the async
and await
keywords.
Great catch! First off, I do agree that this looks questionable. Even after the explanation I give below, it may be beneficial to add the async
/await
keywords back just so this doesn’t catch people off guard.
In short, we can safely omit async
/await
since the calling method, GetRecordAsync
, is ultimately returning the same thing as what it’s calling, _recordRepository.GetAsync
. Instead of our method awaiting the completion of the underlying task and immediately returning the result, it just hands the task to the caller, basically saying, “Here, you await this instead of me”.
The asynchronous nature of / ability to await
a method depends on it’s return type being Task
or Task<T>
(actually the compiler uses duck typing to know which methods are awaitable, but I’ve simplified). You can await
a Task
because it exposes a callback API that allows the framework to tell it what code to execute after whatever asynchronous operation finishes.
Adding the async
keyword to a method declaration does something else that’s very much under the covers. Asynchronous methods operate in repeating chunks of:
await
some I/O workWhen the compiler sees an async
method, it creates a new state machine class that has a state for each chunk of code to be executed. Each state runs the synchronous work it needs, starts some asynchronous I/O, then returns, allowing the thread to go execute other code somewhere else (like responding to another HTTP request). Eventually, the framework will come back around to the state machine (can be triggered by a few reasons) to see if the previous asynchronous operation has finished. If so it will process the next chunk.
Removing the async
/await
keywords prevents the generation of the state machine. This slightly decreases overhead of running the method. TBH, I wouldn’t be surprised if the compiler is smart enough to optimize around this whole thing anyway.
async
/await
After getting feedback on this post, I learned that you can run into several hard to understand bugs if the keywords are omitted by default.
Check out the runnable demo code here!
Consider the following chain of methods:
async Task Top()
{
await Middle();
}
Task Middle() // keywords omitted here
{
return Bottom();
}
async Task Bottom()
{
await Task.Delay(10);
throw new Exception("Bottom Exception");
}
When calling Top()
, the resulting stack trace won’t include the call to Middle()
:
System.Exception: Bottom Exception
at OmittingAsyncDemo.WithoutKeywords.Bottom()
at OmittingAsyncDemo.WithoutKeywords.Top()
at OmittingAsyncDemo.Program.Main(String[] args)
using
The bug in the below code would leave me scratching my head. Calling UsingStatementWithoutKeywords()
results in a TaskCanceledException
.
Task<string> UsingStatementWithoutKeywords()
{
using var client = new HttpClient();
return client.GetStringAsync("https://1.1.1.1");
}
This happens because the HttpClient
is disposed before the request made ever completes, thus canceling any ongoing requests.
Check out more subtle pitfalls of omitting async
/await
in this article by Stephen Cleary.
Equipped with this new information, I would recommend against removing the keywords by default. As they say, premature optimization is the root of all evil. 😅 That’s a little harsh, but I’m sure you catch my drift. I would consider removing the keywords if these conditions are met:
If you find this post useful, and wish to support it, you can below!
Resources: