async, await and Task
What an async method does before its first await, where its exceptions go, and how starting tasks differs from awaiting them.
After this lesson you can
- Say what part of an async method runs synchronously when it is called
- Explain why an exception in an async method surfaces at the await, not the call
- Start several tasks before awaiting them, and await them together with Task.WhenAll
A Task (or Task<T>) is a promise of a result that may not exist
yet. An async method is one the compiler rewrites into a state
machine so that await can suspend it without blocking a thread:
- The caller calls the method. Its body runs synchronously, on the
caller's thread, up to the first
awaitof something that has not finished yet. - At that point the method returns an incomplete
Taskto the caller, and registers the rest of its body as a continuation. - When the awaited operation completes, the continuation runs and
eventually completes the returned
Taskwith a value or an exception.
If every awaited thing is already complete, the method runs to the end synchronously and returns a completed task.
Exceptions are stored in the task
Inside an async method, a thrown exception does not escape the call.
It is caught by the generated state machine and stored in the returned
task, which becomes Faulted. It is rethrown when someone awaits the
task (or calls GetAwaiter().GetResult()). A plain method that returns
a Task has no such machinery: if it throws, it throws at the call.
public static class Solution{ static async Task<int> FailsAsync() { throw new InvalidOperationException("from async"); } static Task<int> FailsEagerly() { throw new InvalidOperationException("from plain method"); } public static string Describe() { var log = new List<string>(); Task<int> t = FailsAsync(); // no exception here log.Add("async call returned, status=" + t.Status); try { t.GetAwaiter().GetResult(); } catch (InvalidOperationException e) { log.Add("thrown when awaited: " + e.Message); } try { FailsEagerly(); log.Add("plain call returned"); } catch (InvalidOperationException e) { log.Add("thrown at the call: " + e.Message); } return string.Join(" | ", log); }}That difference matters for argument checks. In an async method, a
check like if (id < 0) throw ... produces a faulted task, so the
caller only finds out when it awaits, possibly much later. The usual
fix is a non-async public method that validates and then returns the
task of a private async method, which the exercise asks you to write.
Calling starts, awaiting waits
Calling an async method starts its work. await only waits for it. So
await inside a loop runs the operations one after another, while
starting them all first and then awaiting Task.WhenAll lets them
overlap:
public static class Solution{ static async Task<int> Fetch(int id, List<string> log) { lock (log) log.Add("start " + id); await Task.Delay(20); return id * 10; } static async Task<string> RunAsync() { var log = new List<string>(); // One at a time: each await finishes before the next call starts. int seqTotal = 0; foreach (var id in new[] { 1, 2, 3 }) { seqTotal += await Fetch(id, log); lock (log) log.Add("done " + id); } // All at once: calling starts each task; WhenAll waits for them together. var tasks = new[] { 4, 5, 6 }.Select(id => Fetch(id, log)).ToList(); lock (log) log.Add("all started"); int[] results = await Task.WhenAll(tasks); log.Add("results " + string.Join(",", results)); return $"seq total={seqTotal} | " + string.Join(", ", log); } public static string Describe() => RunAsync().GetAwaiter().GetResult();}In the sequential half each start is followed by its done. In the
second half all three starts are logged before all started, because
each call ran synchronously up to its await Task.Delay and returned.
Task.WhenAll then returns the results in the order the tasks were
passed, regardless of which finished first.
Common mistakes
async void. Nothing can await it, and an exception thrown in it cannot be caught by the caller; it is raised on the synchronisation context or thread pool and typically crashes the process. Useasync Task;async voidexists only for event handlers.- Awaiting inside a loop when the operations are independent. They
run one at a time. Start them, then
await Task.WhenAll(tasks). - Validating arguments inside an
asyncmethod. The exception is deferred into the task, as shown above. - Forgetting
await. The call starts the work and the method moves on; an exception in that task goes unobserved. The compiler warns (CS4014) when a task-returning call inside an async method is not awaited.
What interviewers ask
- "Does create a new thread?" No. It suspends the method and frees the current thread; the continuation runs later, on the captured context or a thread-pool thread.
Exercise · Pro
Reject bad arguments at the call, not at the await
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade