Async Programming

By the Syncfusion DevTutorials team · Last updated

Wait without freezing: Task, Task<T>, async and await, how await really runs, return types, ValueTask, async void, exceptions, and async file and network calls.

Introduction to Async Programming

Imagine you are at a busy café ordering coffee.

You place your order. The person at the counter does not just stand and watch the coffee machine until your drink is ready. Instead, they take more orders and help other customers. When your coffee is ready, they call your name.

This is asynchronous work. You start something that takes time, do other things while you wait, and come back when it is finished.

In simple words: async programming lets your program wait without freezing.

Most slow work in real programs is about waiting, not calculating:

  • waiting for a web server to respond,
  • waiting for a file to load from disk,
  • waiting for a database to send data.

C# gives you three main tools for this: Task, async, and await. This lesson will explain all three, step by step.

Easy Way to Remember: a Task is like a receipt for work that will finish later. await means "I will wait here until this receipt is ready".

What You Will Learn

  1. The difference between synchronous and asynchronous code.
  2. Why async exists, and when it helps.
  3. Task and Task<T>, and how to create and start them.
  4. The async and await keywords, and what really happens at an await.
  5. Which return types an async method can have, and when to use ValueTask.
  6. How to design async methods, and why async void is dangerous.
  7. "Start first, await later"—the trick that makes waits overlap.
  8. How exceptions work in async code.
  9. Async file and network operations.

About the Run buttons in this lesson. The examples run in your browser on .NET in WebAssembly. Code that reads real files or calls real web servers cannot run there, so those examples are shown as plain code. The runnable cells use Task.Delay to stand in for the slow part, which behaves exactly the same way.

Start With a Working Example

Press Run.

Make tea while the water boils

using System;
using System.Threading.Tasks;

Console.WriteLine("Put the kettle on");
Task<string> boiling = BoilWaterAsync();

Console.WriteLine("Get a cup");
Console.WriteLine("Add a tea bag");

string water = await boiling;
Console.WriteLine($"Pour the {water}");
Console.WriteLine("Tea is ready");

static async Task<string> BoilWaterAsync()
{
    await Task.Delay(500); // the slow part: 0.5 seconds
    return "hot water";
}

The kettle starts first. While it heats, the program gets the cup and the tea bag. Only when it really needs the water does it await it.

Try This

Move the line string water = await boiling; up, so it comes straight after the kettle starts. The output is the same, but now the program waits before getting the cup. This shows the difference between async code that overlaps work and async code that does not.

Synchronous vs Asynchronous Programming

Synchronous code does one thing at a time. Each line must finish before the next line starts. If a line is slow, everything after it must wait.

Asynchronous code can start a slow operation and let the program keep going. The result is collected later.

Think of two ways to do laundry:

  • Synchronous: start the washing machine, then stand in front of it for an hour.
  • Asynchronous: start the washing machine, cook dinner, and come back when it beeps.
SynchronousAsynchronous
While waitingThe thread is blocked and does nothingThe thread is free for other work
Typical methodstring Read()Task<string> ReadAsync()
How you get the resultIt is returned directlyYou await the task
Good forQuick calculationsWaiting for disk, network, database, timers

Here is the same "two downloads" job written both ways. The synchronous version waits for each one in turn. The async version starts both, then waits once.

One after another, or both at once

using System;
using System.Threading.Tasks;

Console.WriteLine("Async, one after another:");
string a = await DownloadAsync("photo.jpg", 300);
string b = await DownloadAsync("music.mp3", 400);
Console.WriteLine($"Got {a} and {b}");

Console.WriteLine();
Console.WriteLine("Async, both at once:");
Task<string> first = DownloadAsync("photo.jpg", 300);
Task<string> second = DownloadAsync("music.mp3", 400);
Console.WriteLine($"Got {await first} and {await second}");

static async Task<string> DownloadAsync(string name, int ms)
{
    Console.WriteLine($"  start {name}");
    await Task.Delay(ms);
    Console.WriteLine($"  done  {name}");
    return name;
}

In the first half, the second download only starts after the first one is done. In the second half, both start straight away, so their waits overlap. Waits of 300 ms and 400 ms take about 400 ms in total instead of 700 ms.

Why Async Exists

A thread is a worker that runs your code. Every program has a limited number of threads.

When a thread waits for something slow with synchronous code, it is blocked. It cannot do anything else, even though it is not working.

This causes two main problems:

  • Frozen apps. In a desktop or mobile app, one thread draws the screen and handles clicks. If it is blocked for two seconds while a file downloads, the window freezes and shows "Not responding".
  • Wasted servers. A web server gives each request a thread. If every thread is blocked waiting for the database, new visitors have to wait, even though the CPU is almost idle.

Async fixes both. While the slow operation runs, the thread is given back and can do other work. When the result arrives, the method continues where it stopped.

In simple words: async does not make slow work faster. It stops your program from wasting time while it waits.

Async helps when your program is waiting (for disk, network, a database, or a timer). It does not make a heavy calculation run faster. A loop that adds up a billion numbers is just as slow with async in front of it.

Task

A Task is an object that represents work that is running now, or will finish later.

Think of a parcel tracking number. The parcel is not here yet, but the tracking number lets you check on it and know when it arrives.

A Task can tell you:

PropertyWhat it tells you
IsCompletedThe work has finished (in any way)
IsCompletedSuccessfullyThe work finished without an error
IsFaultedThe work failed with an exception
IsCanceledThe work was cancelled
StatusThe current state, such as Running or RanToCompletion

A plain Task does not carry a result. Use it for work that just needs to finish, like saving a file or sending an email.

A Task with no result

using System;
using System.Threading.Tasks;

Task saving = SaveReportAsync();
Console.WriteLine($"Finished yet? {saving.IsCompleted}");

await saving;
Console.WriteLine($"Finished now? {saving.IsCompleted}");
Console.WriteLine($"Status: {saving.Status}");

static async Task SaveReportAsync()
{
    await Task.Delay(200);
    Console.WriteLine("Report saved");
}

Right after the call, the task is still running, so IsCompleted is False. After the await, it has finished.

Task<T>

Task<T> is a task that finishes with a result of type T.

  • Task<int> finishes with an int.
  • Task<string> finishes with a string.
  • Task<List<Order>> finishes with a list of orders.

When you await a Task<T>, you get the T back. The task is the box, and await opens it.

await opens the box and hands you the value

using System;
using System.Threading.Tasks;

Task<int> countTask = CountVisitorsAsync();
int visitors = await countTask;
Console.WriteLine($"Visitors today: {visitors}");

Task<string> nameTask = GetCityAsync();
Console.WriteLine($"City: {await nameTask}");

static async Task<int> CountVisitorsAsync()
{
    await Task.Delay(100);
    return 128; // an int, not a Task<int>
}

static async Task<string> GetCityAsync()
{
    await Task.Delay(100);
    return "Chennai";
}

Inside an async Task<int> method, you return 128;, a plain int. The compiler wraps it in the task for you.

Creating and Running Tasks

There are four common ways to get a task. You will use the first one most of the time.

WayWhen to use it
Call an async method: DownloadAsync()Almost always. Most .NET I/O methods already return tasks
Task.Run(() => ...)Move CPU-heavy work off the current thread
Task.FromResult(value)You already have the answer, but the method must return a task
Task.CompletedTaskA Task method that has nothing to wait for

Four ways to get a task

using System;
using System.Threading.Tasks;

// 1. Call an async method. It starts running straight away.
Task<string> fromMethod = LoadGreetingAsync();

// 2. Task.Run: run a piece of work as a task.
Task<int> fromRun = Task.Run(() =>
{
    int total = 0;
    for (int i = 1; i <= 100; i++) total += i;
    return total;
});

// 3. Task.FromResult: a task that is already finished, with a value.
Task<int> ready = Task.FromResult(7);

// 4. Task.CompletedTask: an already-finished task with no value.
Task nothingToDo = Task.CompletedTask;

Console.WriteLine(await fromMethod);
Console.WriteLine($"Sum 1..100 = {await fromRun}");
Console.WriteLine($"Ready value = {await ready}");
await nothingToDo;
Console.WriteLine($"FromResult finished at once? {ready.IsCompleted}");

static async Task<string> LoadGreetingAsync()
{
    await Task.Delay(100);
    return "Hello from an async method";
}

You rarely need new Task(...) plus .Start(). A task made that way does nothing until you start it, which is a common source of programs that "hang" forever. Call an async method or use Task.Run instead.

async Keyword

The async keyword goes in front of a method's return type:

static async Task<string> LoadNameAsync()

It does two things:

  1. It allows you to use await inside the method.
  2. It tells the compiler to wrap the method's return value in a task. You write return "Asha"; and the caller receives a Task<string>.

async by itself does not make anything run in the background. A method marked async with no await inside runs exactly like a normal method, and the compiler warns you about it (warning CS1998).

By convention, an async method's name ends with Async: LoadNameAsync, SaveAsync, SendEmailAsync. This helps callers see that they should await it.

Easy Way to Remember: async is a permission slip. It allows await, but does nothing on its own.

await Keyword

await means: "if this task is not finished yet, pause this method here, give the thread back, and continue from this line when the task is done."

await does three jobs:

  • It waits for the task, without blocking the thread.
  • It unwraps the result: await on a Task<int> gives you an int.
  • It rethrows any exception the task failed with, so a normal try/catch can handle it.

await waits, unwraps, and keeps the order

using System;
using System.Threading.Tasks;

Console.WriteLine("Checking stock...");
int stock = await GetStockAsync("Pen");
Console.WriteLine($"Pens in stock: {stock}");

if (stock < 50)
{
    Console.WriteLine("Ordering more pens...");
    int ordered = await OrderAsync("Pen", 100);
    Console.WriteLine($"Ordered {ordered} pens");
}

static async Task<int> GetStockAsync(string item)
{
    await Task.Delay(150);
    return 30;
}

static async Task<int> OrderAsync(string item, int amount)
{
    await Task.Delay(150);
    return amount;
}

The code reads top to bottom like normal code. That is the main point of await: async code that looks like regular code.

Execution Flow of await

What really happens at an await? Follow the numbers in this program.

Who runs when?

using System;
using System.Threading.Tasks;

Console.WriteLine("1. Main: calling CookAsync");
Task cooking = CookAsync();
Console.WriteLine("4. Main: CookAsync gave back a task, Main carries on");
await cooking;
Console.WriteLine("7. Main: cooking is finished");

static async Task CookAsync()
{
    Console.WriteLine("2. CookAsync: starts running at once");
    Console.WriteLine("3. CookAsync: reaches await, pauses, returns a task");
    await Task.Delay(300);
    Console.WriteLine("5. CookAsync: resumes after the delay");
    Console.WriteLine("6. CookAsync: done");
}

Step by step:

  1. Calling CookAsync() runs its code straight away, on the same thread, like any method.
  2. At await Task.Delay(300) the delay is not finished, so CookAsync pauses and returns an unfinished task to its caller.
  3. The caller carries on (line 4) until it reaches its own await.
  4. When the delay ends, CookAsync resumes from the line after its await.
  5. When CookAsync finishes, its task completes, and Main resumes after await cooking.

If the awaited task is already finished, await does not pause at all. It just takes the result and keeps going.

An async method runs synchronously until its first await on something unfinished. Code before that first await runs immediately when you call the method, not later.

Async Method Return Types

An async method can return one of these types:

Return typeUse it whenThe caller gets
TaskThe method does work but has no resultSomething to await
Task<T>The method produces a valueA T after await
ValueTask / ValueTask<T>The method often finishes without waiting (see the next section)The same as Task / Task<T>
voidOnly for event handlers, such as a button clickNothing to await (see "Avoiding async void")
IAsyncEnumerable<T>The method returns many values over timeValues with await foreach (lesson 3)

Task and Task<T> side by side

using System;
using System.Threading.Tasks;

await LogAsync("Starting");          // Task: nothing comes back
double price = await GetPriceAsync(); // Task<double>: a value comes back
await LogAsync($"Price is {price:0.00}");

static async Task LogAsync(string message)
{
    await Task.Delay(50);
    Console.WriteLine($"[log] {message}");
}

static async Task<double> GetPriceAsync()
{
    await Task.Delay(50);
    return 249.5;
}

Task vs ValueTask

Every Task is an object. Creating it uses a small amount of memory.

Usually that does not matter. But some methods are called millions of times and usually already have the answer—for example, a method that reads from a cache and only goes to the database when the cache is empty.

ValueTask<T> avoids creating a Task object when the result is already available. When the method does have to wait, it behaves like a normal task.

ValueTask: no extra object when the answer is cached

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

var cache = new Dictionary<int, string>();

Console.WriteLine(await GetUserAsync(1)); // not cached: waits
Console.WriteLine(await GetUserAsync(1)); // cached: no waiting
Console.WriteLine(await GetUserAsync(2)); // not cached: waits

async ValueTask<string> GetUserAsync(int id)
{
    if (cache.TryGetValue(id, out string? name))
        return $"{name} (from cache)";

    await Task.Delay(100); // pretend database call
    name = id == 1 ? "Asha" : "Ravi";
    cache[id] = name;
    return $"{name} (from database)";
}
Task<T>ValueTask<T>
MemoryAlways an objectNo object when the result is ready at once
Can be awaited more than onceYesNo, await it only once
Can be stored and awaited laterYesAvoid it
Default choiceYesOnly for hot paths that usually finish at once

Easy Way to Remember: use Task by default. Use ValueTask only if you have measured and found a real performance problem.

Async Method Design

Good async methods follow a few simple rules:

  1. End the name with Async. For example, GetOrdersAsync, not GetOrders.
  2. Return Task or Task<T>. Not void.
  3. Go async all the way. If a method awaits something, it should itself be async, and so should its caller, up to Main or the event handler.
  4. Do not block. Never call .Result or .Wait() on an unfinished task. Use await.
  5. Keep CPU work and waiting work separate. Use Task.Run for heavy calculation, and the real async APIs for I/O.

Async all the way up

using System;
using System.Threading.Tasks;

// Main awaits the top method...
decimal total = await GetCartTotalAsync(cartId: 42);
Console.WriteLine($"Cart total: {total}");

// ...which awaits the methods below it.
static async Task<decimal> GetCartTotalAsync(int cartId)
{
    int[] itemIds = await GetItemIdsAsync(cartId);
    decimal total = 0;
    foreach (int id in itemIds)
        total += await GetPriceAsync(id);
    return total;
}

static async Task<int[]> GetItemIdsAsync(int cartId)
{
    await Task.Delay(50);
    return new[] { 1, 2, 3 };
}

static async Task<decimal> GetPriceAsync(int itemId)
{
    await Task.Delay(50);
    return itemId * 100m;
}

Blocking on a task might seem easier because it avoids changing the caller:

// Don't do this
decimal total = GetCartTotalAsync(42).Result;
GetCartTotalAsync(42).Wait();

In desktop and older ASP.NET apps, this can deadlock: the program freezes forever. In this browser playground, it fails at once with "Cannot wait on monitors on this runtime". Use await instead.

Avoiding async void

An async void method returns nothing. The caller gets no task, so it:

  • cannot wait for the method to finish,
  • cannot catch its exceptions. An exception inside an async void method can crash the whole program.
// Don't do this
static async void SaveAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Disk full");
}

try
{
    SaveAsync();           // no task to await
}
catch (Exception ex)       // never reached: the exception escapes and crashes the app
{
    Console.WriteLine(ex.Message);
}

The fix is simple: return Task instead of void, and await the call:

Return Task, and the error can be caught

using System;
using System.Threading.Tasks;

try
{
    await SaveAsync();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Save failed: {ex.Message}");
}
Console.WriteLine("The program is still running");

static async Task SaveAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Disk full");
}

The only place async void is acceptable is for event handlers, such as a button's Click method, because its signature must return void. Even there, put a try/catch around the body.

Start First, Await Later

Calling an async method starts the work. The await is only where you collect the result.

So when two operations do not depend on each other, start both, then await both. Their waits overlap.

Breakfast: one by one, or all at once

using System;
using System.Threading.Tasks;

Console.WriteLine("--- One by one ---");
await MakeAsync("toast", 300);
await MakeAsync("eggs", 300);
await MakeAsync("coffee", 300);

Console.WriteLine("--- All at once ---");
Task toast = MakeAsync("toast", 300);
Task eggs = MakeAsync("eggs", 400);
Task coffee = MakeAsync("coffee", 200);
await toast;
await eggs;
await coffee;
Console.WriteLine("Breakfast is ready");

static async Task MakeAsync(string food, int ms)
{
    Console.WriteLine($"Start {food}");
    await Task.Delay(ms);
    Console.WriteLine($"{food} ready");
}

One by one takes about 900 ms. All at once takes about 400 ms, the time of the slowest item. The coffee finishes first because it is the quickest, even though it was started last.

Lesson 2 shows Task.WhenAll, which awaits a whole group of tasks in one line.

Exception Handling in Async Code

When an async method throws, the exception does not come out immediately. It is stored inside the task, and the task becomes faulted.

The exception is thrown again at the await. So an ordinary try/catch around the await works, just like in synchronous code.

The exception travels inside the task

using System;
using System.Threading.Tasks;

Task<decimal> payment = ChargeAsync(-50);
Console.WriteLine("ChargeAsync was called; nothing has been thrown yet");

try
{
    decimal charged = await payment;
    Console.WriteLine($"Charged {charged}");
}
catch (ArgumentException ex)
{
    Console.WriteLine($"Caught at the await: {ex.Message}");
}

Console.WriteLine($"Task faulted? {payment.IsFaulted}");

static async Task<decimal> ChargeAsync(decimal amount)
{
    await Task.Delay(100);
    if (amount <= 0)
        throw new ArgumentException("Amount must be positive");
    return amount;
}

Two rules follow from this:

  • Always await your tasks. A task that is never awaited keeps its exception to itself, and the error is silently lost.
  • finally works too. Use it to clean up whether the operation succeeded or failed.

try, catch and finally with await

using System;
using System.Threading.Tasks;

await UploadAsync("report.pdf", fail: false);
await UploadAsync("huge-video.mp4", fail: true);

static async Task UploadAsync(string file, bool fail)
{
    Console.WriteLine($"Uploading {file}");
    try
    {
        await Task.Delay(100);
        if (fail) throw new InvalidOperationException("File too large");
        Console.WriteLine("  Upload complete");
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"  Upload failed: {ex.Message}");
    }
    finally
    {
        Console.WriteLine("  Connection closed");
    }
}

Async File Operations

Reading and writing files is a classic place for async. The disk is slow compared with the CPU, so the thread should not sit waiting for it.

.NET has an async version of every common file method:

SynchronousAsynchronous
File.ReadAllTextFile.ReadAllTextAsync
File.WriteAllTextFile.WriteAllTextAsync
File.ReadAllLinesFile.ReadAllLinesAsync
File.AppendAllTextFile.AppendAllTextAsync
StreamReader.ReadLineStreamReader.ReadLineAsync
StreamWriter.WriteLineStreamWriter.WriteLineAsync

This program writes a file and reads it back. Run it on your own computer, because the browser cannot reach your disk.

using System;
using System.IO;
using System.Threading.Tasks;

string path = "notes.txt";

await File.WriteAllTextAsync(path, "Buy milk\nCall Ravi\n");
await File.AppendAllTextAsync(path, "Pay rent\n");

string[] lines = await File.ReadAllLinesAsync(path);
Console.WriteLine($"The file has {lines.Length} lines:");
foreach (string line in lines)
    Console.WriteLine($"- {line}");

Output:

The file has 3 lines:
- Buy milk
- Call Ravi
- Pay rent

The stream classes work the same way on any kind of stream. Here they read and write a MemoryStream, a stream that lives in memory, so it runs in the browser too:

Async writing and reading with streams

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;

using var memory = new MemoryStream();

using (var writer = new StreamWriter(memory, Encoding.UTF8, leaveOpen: true))
{
    await writer.WriteLineAsync("Buy milk");
    await writer.WriteLineAsync("Call Ravi");
    await writer.WriteLineAsync("Pay rent");
}

memory.Position = 0; // go back to the start before reading

using var reader = new StreamReader(memory);
int number = 1;
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
    Console.WriteLine($"{number}. {line}");
    number++;
}

Swap the MemoryStream for File.OpenRead("notes.txt") and the same loop reads a real file line by line.

Async Network Operations

Network calls are often the slowest thing a program does. A web request can take hundreds of milliseconds, and the thread should never sit blocked for that long.

HttpClient is the .NET class for web requests, and all of its methods are async.

using System;
using System.Net.Http;
using System.Threading.Tasks;

using var client = new HttpClient();

string html = await client.GetStringAsync("https://example.com");
Console.WriteLine($"Downloaded {html.Length} characters");

HttpResponseMessage response = await client.GetAsync("https://example.com/missing");
Console.WriteLine($"Status: {(int)response.StatusCode}");

This needs a real internet connection and a server that allows the request, so it is shown as plain code. Run it in a console app on your own computer.

MethodWhat it returns
GetStringAsync(url)The body as a string
GetByteArrayAsync(url)The body as bytes, for images and files
GetAsync(url)The full response: status code, headers and body
PostAsync(url, content)Sends data and returns the response

The pattern is always the same, so you can practise it with a pretend service. Here, Task.Delay stands in for the time the network takes:

Calling a (pretend) weather service

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

var weather = new FakeWeatherService();

foreach (string city in new[] { "Chennai", "Delhi", "Atlantis" })
{
    try
    {
        int temperature = await weather.GetTemperatureAsync(city);
        Console.WriteLine($"{city}: {temperature} °C");
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"{city}: request failed ({ex.Message})");
    }
}

class FakeWeatherService
{
    private readonly Dictionary<string, int> _data = new()
    {
        ["Chennai"] = 33,
        ["Delhi"] = 28,
    };

    public async Task<int> GetTemperatureAsync(string city)
    {
        await Task.Delay(150); // pretend network time
        if (!_data.TryGetValue(city, out int temperature))
            throw new HttpRequestException("404 Not Found");
        return temperature;
    }
}

HttpClient throws an HttpRequestException when a request fails, so real code catches it the same way.

Common mistakes

MistakeWrongCorrectWhy
Forgetting to awaitSaveAsync();await SaveAsync();The program moves on early and errors are lost
Blocking on a taskGetNameAsync().Resultawait GetNameAsync()Blocks the thread and can freeze the app
Using async voidasync void SendAsync()async Task SendAsync()Errors cannot be caught and can crash the app
Awaiting independent work one by oneawait A(); await B();Start A and B, then await bothThe waits add up instead of overlapping
Sleeping in async codeThread.Sleep(1000);await Task.Delay(1000);Thread.Sleep blocks the thread
async with no awaitasync Task<int> Get() => 5;Task.FromResult(5)Runs synchronously; warning CS1998

What to Remember

  • Async lets a program wait without freezing. It helps with waiting, not with heavy calculation.
  • A Task is work that finishes later. Task<T> finishes with a value of type T.
  • async allows await and wraps the return value in a task. On its own it does nothing.
  • await waits without blocking, unwraps the result, and rethrows any exception.
  • An async method runs normally until its first await on unfinished work, then returns a task to its caller.
  • Return Task or Task<T>. Use async void only for event handlers.
  • Use ValueTask only for hot methods that usually finish at once.
  • Start independent work first, await it later, so the waits overlap.
  • Exceptions are stored in the task and thrown at the await, so try/catch works as normal.
  • Use the ...Async versions of file and network methods, and never block with .Result, .Wait(), or Thread.Sleep.

Next: Task Coordination