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
- The difference between synchronous and asynchronous code.
- Why async exists, and when it helps.
TaskandTask<T>, and how to create and start them.- The
asyncandawaitkeywords, and what really happens at anawait. - Which return types an async method can have, and when to use
ValueTask. - How to design async methods, and why
async voidis dangerous. - "Start first, await later"—the trick that makes waits overlap.
- How exceptions work in async code.
- 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.
| Synchronous | Asynchronous | |
|---|---|---|
| While waiting | The thread is blocked and does nothing | The thread is free for other work |
| Typical method | string Read() | Task<string> ReadAsync() |
| How you get the result | It is returned directly | You await the task |
| Good for | Quick calculations | Waiting 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:
| Property | What it tells you |
|---|---|
IsCompleted | The work has finished (in any way) |
IsCompletedSuccessfully | The work finished without an error |
IsFaulted | The work failed with an exception |
IsCanceled | The work was cancelled |
Status | The 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 anint.Task<string>finishes with astring.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.
| Way | When 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.CompletedTask | A 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:
- It allows you to use
awaitinside the method. - It tells the compiler to wrap the method's return value in a task. You write
return "Asha";and the caller receives aTask<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:
awaiton aTask<int>gives you anint. - It rethrows any exception the task failed with, so a normal
try/catchcan 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:
- Calling
CookAsync()runs its code straight away, on the same thread, like any method. - At
await Task.Delay(300)the delay is not finished, soCookAsyncpauses and returns an unfinished task to its caller. - The caller carries on (line 4) until it reaches its own
await. - When the delay ends,
CookAsyncresumes from the line after itsawait. - When
CookAsyncfinishes, its task completes, andMainresumes afterawait 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 type | Use it when | The caller gets |
|---|---|---|
Task | The method does work but has no result | Something to await |
Task<T> | The method produces a value | A T after await |
ValueTask / ValueTask<T> | The method often finishes without waiting (see the next section) | The same as Task / Task<T> |
void | Only for event handlers, such as a button click | Nothing to await (see "Avoiding async void") |
IAsyncEnumerable<T> | The method returns many values over time | Values 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> | |
|---|---|---|
| Memory | Always an object | No object when the result is ready at once |
| Can be awaited more than once | Yes | No, await it only once |
| Can be stored and awaited later | Yes | Avoid it |
| Default choice | Yes | Only 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:
- End the name with
Async. For example,GetOrdersAsync, notGetOrders. - Return
TaskorTask<T>. Notvoid. - Go async all the way. If a method awaits something, it should itself be async, and so should its caller, up to
Mainor the event handler. - Do not block. Never call
.Resultor.Wait()on an unfinished task. Useawait. - Keep CPU work and waiting work separate. Use
Task.Runfor 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 voidmethod 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.
finallyworks 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:
| Synchronous | Asynchronous |
|---|---|
File.ReadAllText | File.ReadAllTextAsync |
File.WriteAllText | File.WriteAllTextAsync |
File.ReadAllLines | File.ReadAllLinesAsync |
File.AppendAllText | File.AppendAllTextAsync |
StreamReader.ReadLine | StreamReader.ReadLineAsync |
StreamWriter.WriteLine | StreamWriter.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.
| Method | What 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
| Mistake | Wrong | Correct | Why |
|---|---|---|---|
| Forgetting to await | SaveAsync(); | await SaveAsync(); | The program moves on early and errors are lost |
| Blocking on a task | GetNameAsync().Result | await GetNameAsync() | Blocks the thread and can freeze the app |
| Using async void | async void SendAsync() | async Task SendAsync() | Errors cannot be caught and can crash the app |
| Awaiting independent work one by one | await A(); await B(); | Start A and B, then await both | The waits add up instead of overlapping |
| Sleeping in async code | Thread.Sleep(1000); | await Task.Delay(1000); | Thread.Sleep blocks the thread |
async with no await | async 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
Taskis work that finishes later.Task<T>finishes with a value of typeT. asyncallowsawaitand wraps the return value in a task. On its own it does nothing.awaitwaits without blocking, unwraps the result, and rethrows any exception.- An async method runs normally until its first
awaiton unfinished work, then returns a task to its caller. - Return
TaskorTask<T>. Useasync voidonly for event handlers. - Use
ValueTaskonly 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, sotry/catchworks as normal. - Use the
...Asyncversions of file and network methods, and never block with.Result,.Wait(), orThread.Sleep.
Next: Task Coordination