DEV Community

Cover image for C# Async/Await Made Simple
Lou Creemers
Lou Creemers

Posted on

C# Async/Await Made Simple

Hi lovely readers,

Today we are looking at async and await. If these two words have ever shown up in your code and made you a little nervous, do not worry. This post is written for people who are seeing async/await for the very first time, so we will take it slow and build things up step by step.

The problem async/await solves

Let's start with why this exists at all, using a simple example from real life.

Imagine you are cooking dinner and you put a pot of water on the stove to boil. If you just stood there, staring at the pot until it boiled, you would waste a lot of time. A better approach is to start the water boiling, and while you wait, you chop vegetables or set the table. When the water is ready, you go back to it.

That is exactly what async/await lets your code do. When your application needs to do something that takes time, like reading from a database or calling a web page, it does not have to freeze and wait. It can start that task, do other useful work, and come back to it once it is done.

Without async/await, your program has to wait for one task to fully finish before it can do anything else. This is called blocking, and it can make an application feel slow, especially when many people are using it at the same time.

The two keywords, explained simply

async is a label you put on a method to say: "this method contains something that might take time, and it is allowed to pause halfway through."

await is the actual pause. It is the exact place where your code says: "wait here until this is finished, but do not block anything else while you wait."

Here is a small example:

public async Task<User> GetUserAsync(int id)
{
    // This is where we "wait" for the database to respond
    var user = await _dbContext.Users.FindAsync(id);

    // Once the database has answered, we continue from here
    return user;
}
Enter fullscreen mode Exit fullscreen mode

Notice three things:

  1. The method has async in its signature.
  2. It returns a Task<User> instead of just User. This is normal for async methods. Think of Task<User> as a promise that a User will be delivered eventually.
  3. The await keyword is placed right before the slow operation, FindAsync.

How do you call an async method?

To call an async method, you also need to use await, and the method that is calling it usually needs to be async too.

public async Task PrintUserNameAsync(int id)
{
    var user = await GetUserAsync(id);
    Console.WriteLine(user.Name);
}
Enter fullscreen mode Exit fullscreen mode

This is a pattern you will see everywhere in C#. Once one method becomes async, the methods that call it usually become async as well. This is completely normal, so do not worry if it feels like async spreads through your code. That is simply how it works.

A few beginner-friendly rules to hold onto

Always return Task or Task<T> from your async methods, never void. The only exception is event handlers, which is a more advanced topic you will run into later. If something goes wrong inside a method that returns void, the error can get lost and your application might crash without a clear reason.

// Avoid this
public async void ProcessOrderAsync(Order order)
{
    await _repository.SaveAsync(order);
}

// Do this instead
public async Task ProcessOrderAsync(Order order)
{
    await _repository.SaveAsync(order);
}
Enter fullscreen mode Exit fullscreen mode

Never call .Result or .Wait() on a task. It might be tempting to use these when you are not in an async method yourself, but they force your program to block and wait, which is exactly what async/await is trying to avoid. In some situations, this can even cause your application to freeze completely. Stick to await.

// Avoid this, it can cause your app to freeze
var user = GetUserAsync(id).Result;

// Use this instead
var user = await GetUserAsync(id);
Enter fullscreen mode Exit fullscreen mode

If you have several tasks that do not depend on each other, you can run them at the same time. Waiting for them one by one when you do not need to is a common beginner habit, and it is an easy thing to improve once you notice it.

// One after another, even though they do not depend on each other
foreach (var order in orders)
{
    await ProcessSingleOrderAsync(order);
}

// All at once instead
var tasks = orders.Select(order => ProcessSingleOrderAsync(order));
await Task.WhenAll(tasks);
Enter fullscreen mode Exit fullscreen mode

What's next?

Once you feel comfortable with the basics, there are a few more advanced topics worth exploring later on, like ConfigureAwait, the difference between Task and ValueTask, and async streams with IAsyncEnumerable. You do not need any of that to get started, though. For now, focus on getting comfortable with async, await, and Task, since that foundation will carry you a long way.

That's a wrap!

Async/await can feel a bit strange the first time you use it, and that is completely normal. Give yourself time to get used to it. The short version to remember: async marks a method that can pause, await is where it pauses, and Task is the promise that something will be ready eventually.

If you have thoughts, questions, or you are working through your own first async method right now, feel free to leave a comment or reach out to me on my socials. I remember exactly how confusing this felt at first, so do not hesitate to ask anything.

Top comments (0)