Skip to content

How the await Keyword Works

If async prepares a method, await is where the real work happens. In simple words, await means:

"If this task is not finished yet, pause this method here, give the thread back so it can do other work, and continue from the next line when the task is done."

What happens at an await, step by step

When the program reaches await someTask:

  1. Check: is someTask already finished?
    • Yes: take the result and continue immediately, like normal code. No pause at all.
    • No: go to step 2.
  2. Save: remember where the method stopped and the values of its local variables.
  3. Register: tell the task "when you finish, run the rest of this method".
  4. Return: the method returns an unfinished Task to its caller. The thread is now free.
  5. Resume: when the task finishes, the rest of the method runs from the line after await.
  6. Unwrap: await gives you the result of the task (for Task<T>), or throws the task's exception.

await unwraps the result

Task<string> task = File.ReadAllTextAsync("notes.txt");   // a Task<string>
string text = await task;                                   // a string

// Usually written in one line
string text2 = await File.ReadAllTextAsync("notes.txt");

Without await you only have the "receipt" (Task<string>). With await you get the actual value (string).

await does not block the thread

The word "wait" is in await, but it does not block. Compare:

CodeWaits for result?Blocks the thread?
await taskYesNo: the thread is free while waiting
task.ResultYesYes: the thread is stuck until it finishes
task.Wait()YesYes

The method pauses, but the thread does not. That is the most important idea in this whole series.

Sequential awaits vs concurrent awaits

Where you put await decides whether tasks run one after another or at the same time.

static async Task<string> GetDataAsync(string name, int ms)
{
    await Task.Delay(ms);
    return $"{name} loaded";
}

// 1. Sequential: the second call starts only after the first finishes.
//    Total: 2 + 3 = about 5 seconds
string customers = await GetDataAsync("Customers", 2000);
string orders = await GetDataAsync("Orders", 3000);

// 2. Concurrent: start both first, await later.
//    Total: about 3 seconds (the longest one)
Task<string> customersTask = GetDataAsync("Customers", 2000);
Task<string> ordersTask = GetDataAsync("Orders", 3000);
string customers2 = await customersTask;
string orders2 = await ordersTask;

// 3. Concurrent with Task.WhenAll (cleanest)
string[] all = await Task.WhenAll(
    GetDataAsync("Customers", 2000),
    GetDataAsync("Orders", 3000));

Use sequential awaits when the second call needs the result of the first (for example, get the user, then get that user's orders). Use concurrent awaits when the calls are independent.

await and exceptions

If the awaited task failed, await throws the original exception, so a normal try/catch works:

try
{
    string page = await httpClient.GetStringAsync("https://example.com/missing-page");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Request failed: {ex.Message}");
}

With .Result or .Wait(), the exception comes wrapped in an AggregateException, which is less convenient. Another reason to prefer await.

When you await Task.WhenAll and several tasks fail, await throws only the first exception. The others are still available in the task's Exception.InnerExceptions property.

Where the code continues after await

  • WinForms / WPF / MAUI: after await, the code continues on the UI thread, so you can safely update labels and text boxes.
  • ASP.NET Core and console apps: the code continues on any free thread-pool thread.
  • Library code: you can write await task.ConfigureAwait(false) to say "I don't need to come back to the original context". This avoids some deadlocks and is slightly faster.

Rules for await

  • await can only be used in an async method (or in top-level statements in Program.cs).
  • You can await Task, Task<T>, ValueTask, ValueTask<T>, and any type that has a GetAwaiter() method.
  • await works inside try, catch and finally blocks (C# 6+).
  • await is not allowed inside a lock block. Use SemaphoreSlim with await semaphore.WaitAsync() instead.
  • Use await foreach to loop over an IAsyncEnumerable<T>, and await using for objects that are disposed asynchronously.

Summary

  • await pauses the method, not the thread.
  • If the task is already complete, await just continues without pausing.
  • It unwraps the result of Task<T> and re-throws the original exception.
  • Start independent tasks first and await them later (or use Task.WhenAll) to run them concurrently.
  • In UI apps the code after await returns to the UI thread; in ASP.NET Core it runs on any pool thread.