Async/Await
Async Programming
Introduction
In this lesson, you'll learn about async/await in C#. Coming from TypeScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In TypeScript, you're familiar with async programming.
C# has its own approach to async programming, which we'll explore step by step.
The C# Way
Let's see how C# handles this concept. Here's a typical example:
async Task<User> FetchUser(int id) {
var res = await httpClient.GetAsync($"/api/users/{id}");
if (!res.IsSuccessStatusCode) throw new Exception("Not found");
return await res.Content.ReadFromJsonAsync<User>();
}
async Task<User[]> LoadAll(int[] ids) {
return await Task.WhenAll(ids.Select(FetchUser));
}Comparing to TypeScript
Here's how you might have written similar code in TypeScript:
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error("Not found");
return res.json() as Promise<User>;
}
async function loadAll(ids: number[]): Promise<User[]> {
return Promise.all(ids.map(fetchUser));
}You may be used to different syntax or behavior.
async/await syntax is nearly identical between TypeScript and C#
You may be used to different syntax or behavior.
Promise<T> → Task<T> in C#
You may be used to different syntax or behavior.
Promise.all() → Task.WhenAll()
You may be used to different syntax or behavior.
void async functions return Task (not Promise<void>), allowing awaiting
Step-by-Step Breakdown
1. Task<T> = Promise<T>
C# Task<T> is the exact equivalent of TypeScript's Promise<T>. Async methods return Task<T> instead of Promise<T>. The async/await keywords work identically.
async function getUser(): Promise<User> {
return await fetch("/user").then(r => r.json());
}async Task<User> GetUser() {
var res = await httpClient.GetAsync("/user");
return await res.Content.ReadFromJsonAsync<User>();
}2. Task.WhenAll = Promise.all
Task.WhenAll() runs multiple tasks concurrently and awaits all of them, exactly like Promise.all().
const users = await Promise.all([fetchUser(1), fetchUser(2)]);var users = await Task.WhenAll(FetchUser(1), FetchUser(2));3. async void — Avoid It
C# allows async void methods (for event handlers) but they cannot be awaited and exceptions are unhandled. Always use async Task instead of async void.
// TypeScript: no void async distinction
async function doWork(): Promise<void> {}// C#: prefer Task over void
async Task DoWork() {} // awaitable
// async void DoWork() {} // avoid — not awaitable4. ConfigureAwait
In library code, add .ConfigureAwait(false) after awaits to avoid context deadlocks. Not needed in ASP.NET Core (no SynchronizationContext) but good practice in libraries.
// TypeScript has no equivalent — runs in single-threaded event loop// In library code:
var data = await GetDataAsync().ConfigureAwait(false);
// Prevents deadlocks in WinForms/WPF contextCommon Mistakes
When coming from TypeScript, developers often make these mistakes:
- async/await syntax is nearly identical between TypeScript and C#
- Promise<T> → Task<T> in C#
- Promise.all() → Task.WhenAll()
Key Takeaways
- async/await syntax is nearly identical in TypeScript and C# — they share the same design
- Promise<T> → Task<T>, Promise.all → Task.WhenAll
- Avoid async void — use async Task instead
- Use ConfigureAwait(false) in library code to prevent context deadlocks