async and await
async marks a method that may pause; await pauses it, hands control back to the caller, and resumes it when the task finishes.
async Task<TResult> DoWorkAsync(...) { ... await somethingAsync; return value; }async and await are two keywords that work as a pair. async on a method says this method is allowed to pause. await is where it pauses: it takes a Task and means "continue from here once that task has finished".
What the compiler does
The compiler rewrites an async method into a state machine: a hidden type that records which point in the method you reached and the values of the local variables. The body runs normally until it meets an await whose task is not already finished. At that point the method returns to its caller, handing back its own Task. The remainder of the method is registered as the continuation, the code to run when the awaited task completes.
You still read the method top to bottom. That is the whole purpose of the rewrite. The code looks sequential and the pauses are invisible in the text.
Return types
An async method returns Task when it produces no value, Task<T> when it produces one, and the value in a return statement is the T, not the Task. By convention the method name ends in Async.
What trips people up
Calling an async method starts no thread. Its body runs on the calling thread until the first unfinished await. And await on an already-finished task does not pause at all; the method just keeps going on the same thread.
Example
Console.WriteLine("1 before call");
// Calling StepsAsync runs its body right here, on this thread,
// until the first await that is not already finished.
Task<int> work = StepsAsync();
Console.WriteLine("3 after call, before await");
// Now this method pauses until work completes.
int result = await work;
Console.WriteLine($"5 result {result}");
static async Task<int> StepsAsync()
{
Console.WriteLine("2 inside, before await");
// Task.Delay is not finished, so StepsAsync returns to its caller
// here and hands back a Task<int> that is not yet complete.
await Task.Delay(100);
// This line is the continuation. It runs when the delay elapses.
Console.WriteLine("4 inside, after await");
return 42;
}
// Output:
// 1 before call
// 2 inside, before await
// 3 after call, before await
// 4 inside, after await
// 5 result 42When to use it
- Reading a file or calling a service inside a method without blocking the caller's thread.
- Starting two independent operations, then awaiting each where its result is first needed.
- Wrapping an awaited call in try/catch so a failed network call becomes a handled error.
More examples
await and the callback it replaces
These two methods do the same job, and the second is roughly what people wrote before C# 5. Recognising the ContinueWith shape matters because it is still all over older codebases and in some library internals. Write the first form; read the second.
// Shorthand: await. The compiler writes the callback for you.
static async Task<int> CountAsync(string path)
{
string text = await File.ReadAllTextAsync(path);
return text.Length;
}
// Longhand: the same chaining done by hand with ContinueWith.
// ContinueWith hands the finished task to the callback, so the result
// is read off it. Failures arrive wrapped in an AggregateException
// rather than as the original exception, which is one reason await won.
static Task<int> CountWithContinuation(string path)
{
return File.ReadAllTextAsync(path)
.ContinueWith(read => read.Result.Length);
}An async method that never awaits
CS1998 is the compiler telling you that a method advertises waiting and then never waits. It is worth fixing rather than suppressing, because the async keyword on a method that cannot pause misleads every reader of the call site. This also shows that async is an implementation detail: dropping it does not change the signature callers see.
public sealed class NameValidator
{
// Marked async but with no await inside. The compiler raises warning
// CS1998 and the method runs entirely synchronously, paying for a
// state machine it does not use.
public async Task<bool> IsValidWrongAsync(string name)
{
return name.Length is > 0 and <= 50;
}
// Same behaviour, no async, no warning: return an already finished
// Task<bool>. These two methods are interchangeable to every caller.
public Task<bool> IsValidAsync(string name)
{
bool valid = name.Length is > 0 and <= 50;
return Task.FromResult(valid);
}
}Errors cross an await normally
The state machine preserves exception handling across the pause, so a catch written before the await still catches a failure that happens after it. This is why async code can be written in the ordinary error-handling style, and it is the behaviour that ContinueWith chains could not give you.
// try/catch works through an await. The exception is captured in the
// task, then rethrown at the await point, inside the try block.
try
{
int value = await ParseAsync("twelve");
Console.WriteLine($"value: {value}");
}
catch (FormatException ex)
{
Console.WriteLine($"rejected: {ex.Message}");
}
static async Task<int> ParseAsync(string text)
{
await Task.Delay(10);
if (!int.TryParse(text, out int parsed))
{
throw new FormatException($"'{text}' is not a number");
}
return parsed;
}
// Output:
// rejected: 'twelve' is not a number
Discussion