# Tasks, async and await

A Task is work that finishes later. An async method can await a task, which frees its thread while it waits for a database or the network, so a web server can keep serving other requests. Await every async call, and name async methods with Async.

Journeyman · C# · ASP.NET Core · Updated November 3, 2026

## After this lesson you can

- Write an async method that awaits other async methods and returns Task or Task&lt;T>
- Explain why a web API awaits database and network calls instead of blocking a thread

## Key takeaways

- A `Task` is work that finishes later, and a `Task<T>` finishes with a `T`. By convention, methods that return tasks end in `Async`; controller actions don't.
- `await` gets a task's result without blocking the thread, so a web server can serve other requests while the database works.
- An `async` method returns `Task` or `Task<T>`, but its `return` gives just the `T`.
- Start several tasks first, then `await Task.WhenAll(...)` to wait for them all at once.
- Await every task and stay async all the way: never block on a task with `.Result` or `.Wait()`.

Think of a waiter in a busy restaurant. They take a table's order to the kitchen. A bad waiter stands at the kitchen door until the food is ready, while every other table waits. A good waiter hands in the order, serves other tables, and comes back when the kitchen calls.

A web server has the same choice, many times a second. A request asks the database for categories. The database takes a few milliseconds, an eternity for a computer. Should the **thread** handling that request (the waiter) stand and wait, or go and serve another request until the answer arrives? With **`async`** and **`await`**, it goes and serves.

## Tasks

A **`Task`** is a piece of work that will finish **later**. A method that starts slow work, such as a query or a download, can return a task straight away instead of making its caller wait:

| Return type | Means |
|---|---|
| `Task` | *"I'll finish later, with nothing to give you."* |
| `Task<int>` | *"I'll finish later, and give you an `int`."* |
| `Task<List<Category>>` | *"I'll finish later, and give you a list of categories."* |

By convention, methods that return tasks end in **`Async`**: `QueryAsync`, `SaveChangesAsync`, `ReadAllTextAsync`.

## await

**`await`** waits for a task to finish and gives you its result, **without blocking the thread** while it waits:

```csharp
Console.WriteLine("Ordering...");
string meal = await CookAsync("pancakes");
Console.WriteLine($"Served: {meal}");

async Task<string> CookAsync(string dish)
{
    await Task.Delay(500);
    return $"hot {dish}";
}
```

```text
Ordering...
Served: hot pancakes
```

- `Task.Delay(500)` is a task that finishes after half a second: a stand-in for a slow database.
- **`async`** on a method allows `await` inside it. An async method returns `Task` or `Task<T>`, but its `return` gives just the `T`: `return $"hot {dish}";` returns a `string`, and C# wraps it in the `Task<string>`.
- **`await CookAsync(...)`** gets the `string` out of the `Task<string>`.

Read top to bottom, the code looks just like ordinary code, and it runs in the same order. The difference is invisible: during each `await`, the thread was free to do other work.

## Starting several tasks at once

A task starts running as soon as it's created. So you can start several, then await them together, the way a waiter puts in three orders before collecting any:

```csharp
using System.Diagnostics;

Stopwatch timer = Stopwatch.StartNew();

Task<string> pancakes = CookAsync("pancakes");
Task<string> eggs = CookAsync("eggs");
Task<string> toast = CookAsync("toast");

string[] meals = await Task.WhenAll(pancakes, eggs, toast);

Console.WriteLine(string.Join(", ", meals));
Console.WriteLine(timer.ElapsedMilliseconds < 1000 ? "Under a second" : "Over a second");

async Task<string> CookAsync(string dish)
{
    await Task.Delay(500);
    return $"hot {dish}";
}
```

```text
hot pancakes, hot eggs, hot toast
Under a second
```

Three half-second dishes in about half a second, because they cooked at the same time. **`Task.WhenAll`** waits for all of them, and gives their results in order.

## Async in a web API

ASP.NET Core actions can be async, and Dapper has async versions of every method. Here's `GetById` from the DAO to the controller:

```csharp
public async Task<Category?> GetByIdAsync(int categoryId)
{
    using SqlConnection connection = new SqlConnection(connectionString);
    return await connection.QuerySingleOrDefaultAsync<Category>(
        "SELECT CategoryId, CategoryName, Description FROM Categories WHERE CategoryId = @CategoryId;",
        new { CategoryId = categoryId });
}
```

```csharp
[HttpGet("{id}")]
public async Task<ActionResult<Category>> GetById(int id)
{
    Category? category = await categoryDao.GetByIdAsync(id);
    if (category == null)
    {
        return NotFound();
    }
    return category;
}
```

The changes are mechanical:

1. **`async`** on the method.
2. The return type wrapped in **`Task<...>`**: `Category?` becomes `Task<Category?>`, `ActionResult<Category>` becomes `Task<ActionResult<Category>>`, `IActionResult` becomes `Task<IActionResult>`.
3. The async version of each call, with **`await`**: `QuerySingleOrDefaultAsync`, `ExecuteAsync`, `QueryAsync`.
4. **`Async`** on the DAO methods' names, and in the interface: `Task<Category?> GetByIdAsync(int categoryId);`.

Controller actions keep their names (`GetById`, not `GetByIdAsync`). ASP.NET Core drops an `Async` ending from action names anyway, so `CreatedAtAction(nameof(GetByIdAsync), ...)` would look for an action that doesn't exist.

While the database works on the query, the request's thread goes back to ASP.NET Core to serve other requests. On a busy server that's the difference between handling hundreds of requests at once and running out of threads. That's why professional ASP.NET Core code is async from the controller all the way down to the database.

## Rules of thumb

- **Await every task.** A task you don't await may fail silently, and the method carries on before the work is done.
- **Async all the way.** Once one method is async, its callers should be too. Never block on a task with `.Result` or `.Wait()`: that holds the thread hostage, which is exactly what async was meant to avoid.
- **`async void` is only for event handlers.** Everywhere else, return `Task`.

## Check yourself

2 questions to try on the lesson page.

## So what

`await` lets a thread serve others while it waits for slow work, and the code still reads top to bottom. Make the DAO async, the controller async, and await every call between them.

## Key terms

- [Thread](https://www.ask-codey.com/glossary/thread-c-sharp)
- [Task](https://www.ask-codey.com/glossary/task)
- [Async/Await](https://www.ask-codey.com/glossary/async-await)

## Cheat sheets

- [Tasks, async and await](https://www.ask-codey.com/cheat-sheets/async-and-await-in-csharp-tasks-async-and-await-cheat-sheet)

Source: https://www.ask-codey.com/lessons/async-and-await-in-csharp-tasks-async-and-await
