Modern C# applications demand non-blocking execution to stay responsive. The async and await keywords introduced in C# 5.0 provide a linear programming model without sacrificing asynchrony. However, understanding control flow, exception handling, and context affinity is crucial to avoid subtle bugs.
Creating an Async Method
Mark a method with the async modifier and return Task, Task<T>, or (only for UI event handlers) void. Inside the method, use await to asynchronously wait for a task without blocking the calling thread. The compiler transforms the rest of the method into a continuation that resumes after the awaited operation completes.
async Task<string> FetchMessageAsync()
{
await Task.Delay(TimeSpan.FromSeconds(2));
return "Operation complete";
}
Sequential and Parallel Awaits
When multiple await expressions apppear in sequence, they execute one after the other. To run several tasks concurrently, start them without awaiting immediately, then use Task.WhenAll.
async Task PerformSequentialWorkAsync()
{
string first = await FetchDataAsync("first");
Console.WriteLine(first);
string second = await FetchDataAsync("second"); // starts only after first finishes
Console.WriteLine(second);
}
async Task PerformConcurrentWorkAsync()
{
Task<string> taskA = FetchDataAsync("A");
Task<string> taskB = FetchDataAsync("B");
string[] results = await Task.WhenAll(taskA, taskB);
foreach (var r in results) Console.WriteLine(r);
}
async Task<string> FetchDataAsync(string label)
{
await Task.Delay(2000);
return $"Data for {label} on thread {Environment.CurrentManagedThreadId}";
}
The behavior appears synchronous yet stays asynchronous: the thread is released during waits.
Handling Exceptions
A direct try/catch works for a single faulted await. For multiple tasks, the AggregateException may contain several inner exceptions. Wrap the combined task and inspect its Exception property.
async Task<string> FaultyOp(string id)
{
await Task.Delay(100);
throw new Exception($"Boom from {id}");
}
async Task ExceptionHandlingExample()
{
Task<string> t1 = FaultyOp("first");
Task<string> t2 = FaultyOp("second");
Task<string[]> combined = Task.WhenAll(t1, t2);
try
{
string[] results = await combined;
}
catch
{
var flattened = combined.Exception?.Flatten();
if (flattened != null)
foreach (var ex in flattened.InnerExceptions)
Console.WriteLine($"Caught: {ex.Message}");
}
}
C# 6.0 allows await inside catch and finally blocks, enabling further asynchronous cleanup or logging.
Synchronization Context and ConfigureAwait
By default, await captures the current SynchronizationContext (e.g., the UI thread) and resumes the continuation on that context. To avoid deadlocks and improve performance for non-UI code, use ConfigureAwait(false).
async Task UpdateButtonTextAsync(Button btn)
{
btn.Text = "Working...";
// capturing context is desired here because we update the button later
await Task.Delay(1000); // resumes on UI thread
btn.Text = "Done";
}
async Task BackgroundCpuWorkAsync()
{
// No context needed; use ConfigureAwait(false) for efficiency
await Task.Delay(500).ConfigureAwait(false);
// continuation on any thread pool thread
}
Dangers of async void
Methods marked async void cannnot be awaited and any unhandled exception crashes the process. Use them exclusively for top-level event handlers. Prefer async Task everywhere else.
// Good: event handler
button.Click += async (s, e) => { await LoadDataAsync(); };
// Bad: general purpose
async void DoDangerousWork()
{
await Task.Delay(100);
throw new InvalidOperationException(); // crashes the app, cannot be caught externally
}
Building a Custom Awaitable
To deeply understand the compiler's requirements, implement your own awaitable type. The pattern needs a GetAwaiter() method returning an object with IsCompleted, GetResult(), and INotifyCompletion implementation.
class MyAwaitable
{
private bool _completeSync;
public MyAwaitable(bool completeSync) => _completeSync = completeSync;
public MyAwaiter GetAwaiter() => new MyAwaiter(_completeSync);
}
class MyAwaiter : INotifyCompletion
{
private string _output = "Sync result";
private bool _isSync;
public bool IsCompleted => _isSync;
public MyAwaiter(bool completeSync) => _isSync = completeSync;
public string GetResult() => _output;
public void OnCompleted(Action continuation)
{
ThreadPool.QueueUserWorkItem(_ =>
{
Thread.Sleep(1000);
_output = $"Thread {Environment.CurrentManagedThreadId}";
continuation();
});
}
}
Usage: string result = await new MyAwaitable(true);
Dynamic Awaitable Objects
Using dynamic and ExpandoObject, you can build awaiters at runtime. A proxy generated by a library like ImpromptuInterface can supply the required interfaces.
dynamic CreateDynamicAwaitable(bool sync)
{
dynamic awaiter = new ExpandoObject();
awaiter.Message = "Dynamic hello";
awaiter.IsCompleted = sync;
awaiter.GetResult = (Func<string>)(() => awaiter.Message);
awaiter.OnCompleted = (Action<Action>)(callback =>
ThreadPool.QueueUserWorkItem(_ =>
{
Thread.Sleep(1000);
awaiter.Message = "Dynamic result";
callback();
})
);
// Use a proxy to fulfil INotifyCompletion
IAwaiter<string> proxy = Impromptu.ActLike(awaiter);
dynamic awaitable = new ExpandoObject();
awaitable.GetAwaiter = (Func<IAwaiter<string>>)(() => proxy);
return awaitable;
}
public interface IAwaiter<T> : INotifyCompletion
{
bool IsCompleted { get; }
T GetResult();
}