Skip to content

How the async Keyword Works

Many beginners think that writing async on a method makes it run in the background or on a new thread. It does not. In this lesson we see exactly what async does and what it does not do.

What async does

The async keyword is an instruction for the compiler. It does four things:

  1. It allows await inside the method. Without async, the word await gives a compile error.
  2. It wraps your return value in a Task. You write return 10; and the caller receives a Task<int>.
  3. It stores exceptions inside the Task. An exception thrown in the method does not fly out immediately; it is kept in the returned task and re-thrown when someone awaits it.
  4. It tells the compiler to rewrite the method into a small "state machine" that can pause at an await and continue later. (We look inside it in How Async Works Internally.)

What async does NOT do

  • It does not create a new thread.
  • It does not make the method run in the background.
  • It does not make slow code faster.

An async method runs synchronously, on the caller's thread, until it reaches the first await of a task that is not finished yet. Only at that point does it pause and return to the caller.

Proof: watch the thread numbers

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine($"1. Main started on thread {Environment.CurrentManagedThreadId}");

        Task task = DoWorkAsync();
        Console.WriteLine("3. Main got the Task back and continues");

        await task;
        Console.WriteLine("5. Main finished");
    }

    static async Task DoWorkAsync()
    {
        Console.WriteLine($"2. DoWorkAsync started on thread {Environment.CurrentManagedThreadId}");

        await Task.Delay(1000);   // the method pauses here and returns to Main

        Console.WriteLine($"4. DoWorkAsync resumed on thread {Environment.CurrentManagedThreadId}");
    }
}
Output (thread numbers can differ)
1. Main started on thread 1
2. DoWorkAsync started on thread 1
3. Main got the Task back and continues
4. DoWorkAsync resumed on thread 7
5. Main finished
  • Line 2 shows that DoWorkAsync started on the same thread as Main. No new thread was created.
  • Line 3 appears before line 4: at the await, DoWorkAsync paused and handed a Task back to Main.
  • Line 4 ran on a different thread. In a console app the code after await continues on any free thread-pool thread. (In WinForms/WPF it would come back to the UI thread.)

An async method without await

If you write async but never use await, the compiler warns you:

static async Task<int> AddAsync(int a, int b)
{
    return a + b;
}
Compiler warning
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.

This method is completely synchronous; async only added a little overhead. If you need to return a Task without any real waiting, return a completed task instead:

static Task<int> AddAsync(int a, int b)
{
    return Task.FromResult(a + b);   // no async keyword needed
}

The return value is wrapped for you

static async Task<int> GetStockAsync(int productId)
{
    await Task.Delay(500);   // pretend: read from the database
    return 25;               // we return int, the caller gets Task<int>
}

// Caller
Task<int> stockTask = GetStockAsync(101);   // the Task
int stock = await stockTask;                 // the int inside it
Console.WriteLine($"Stock: {stock}");

For a method that returns Task (no value), you simply write return; or nothing at all.

Exceptions are stored in the Task

static async Task SaveOrderAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Database is not available");
}

static async Task Main()
{
    Task saveTask = SaveOrderAsync();   // no exception yet
    Console.WriteLine("Save started");

    try
    {
        await saveTask;                 // the exception is thrown here
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }
}
Output
Save started
Error: Database is not available

Why async void is dangerous

An async void method returns nothing, so there is no Task to store its exception. If it fails, the exception cannot be caught by the caller and can crash the whole application. The caller also can't know when it has finished.

// Bad: nobody can await this or catch its errors
async void SendEmail() { ... }

// Good
async Task SendEmailAsync() { ... }

// The only correct use of async void: event handlers
private async void btnSend_Click(object sender, EventArgs e)
{
    try
    {
        await SendEmailAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

Where you can't use async

  • Constructors can't be async. Use a static CreateAsync() factory method instead.
  • Properties can't be async. Use a method such as GetTotalAsync().
  • Async methods can't have ref, out or in parameters.
  • The Main method can be async only from C# 7.1 onwards.

Summary

  • async enables await and makes the compiler build a state machine for the method.
  • It wraps the return value and any exception in the returned Task.
  • It does not create a thread; the method runs synchronously until the first real await.
  • An async method without await gives warning CS1998 and runs synchronously.
  • Use async Task, not async void, except for event handlers.