await does not start a thread. It splits your method into pieces and runs the next piece when a task finishes. Once you see the pieces, deadlocks, ConfigureAwait and async void all make sense.
This page assumes you have written async Task methods and awaited HttpClient calls. It goes under the hood first, then covers what interviews ask about most. That means deadlocks, cancellation, combinators, async streams, channels and exception rules. Every sample is a static method that returns a value. Demos block with .GetAwaiter().GetResult() only so a plain test can check them. Real code should await all the way up.
Several samples need a thread that behaves like a UI thread. It has one queue of work and runs it one item at a time. The class below is that helper. It is a cut-down version of what WinForms, WPF and old ASP.NET install. It is defined once here and reused later.
/// <summary>
/// A SynchronizationContext with one work queue, pumped by whoever calls RunPending.
/// It mimics a UI thread: continuations posted here wait until the pump runs them.
/// </summary>
/// <example>SingleThreadContext.Run(() => Task.FromResult(7)) returns 7</example>
public sealed class SingleThreadContext : SynchronizationContext
{
private readonly Queue<(SendOrPostCallback Work, object? State)> _queue = new();
private readonly object _gate = new();
/// <summary>Queues work. Await continuations arrive here when they capture us.</summary>
public override void Post(SendOrPostCallback d, object? state)
{
lock (_gate) { _queue.Enqueue((d, state)); }
}
/// <summary>How many work items are waiting for the pump.</summary>
public int Pending { get { lock (_gate) { return _queue.Count; } } }
/// <summary>Runs queued work on the calling thread until the queue is empty.</summary>
/// <returns>How many items ran.</returns>
/// <example>After one Post, RunPending() returns 1</example>
public int RunPending()
{
// ran counts finished items. It starts at 0 because nothing ran yet.
int ran = 0;
// Each pass takes one item. The loop ends when the queue is empty.
while (true)
{
(SendOrPostCallback Work, object? State) item;
lock (_gate)
{
if (_queue.Count == 0) return ran;
item = _queue.Dequeue();
}
item.Work(item.State);
ran++;
}
}
/// <summary>
/// Installs a fresh context on this thread, runs body, and pumps until its task ends.
/// This is the "UI message loop" for the demos.
/// </summary>
/// <param name="body">The async work to start on the context.</param>
/// <returns>The task's result.</returns>
/// <example>SingleThreadContext.Run(async () => { await Task.Yield(); return 1; }) returns 1</example>
public static TResult Run<TResult>(Func<Task<TResult>> body)
{
SynchronizationContext? previous = Current;
var ctx = new SingleThreadContext();
SetSynchronizationContext(ctx);
try
{
Task<TResult> task = body();
// Pump until the task is done. Each pass runs what is queued, then naps.
while (!task.IsCompleted)
{
if (ctx.RunPending() == 0) Thread.Sleep(1); // 1 ms nap: avoid a hot spin
}
// Drain once more. An async void method may have posted its exception.
ctx.RunPending();
return task.GetAwaiter().GetResult();
}
finally
{
SetSynchronizationContext(previous);
}
}
}
Task<T> is a class. Every call that returns one allocates it, unless the runtime has a cached instance. ValueTask<T> is a struct. It holds either a ready result or a Task<T>. It can also wrap a pooled IValueTaskSource<T>. When the result is already known, it costs no heap allocation.
Task. It can be awaited many times, stored, passed to WhenAll, and blocked on.ValueTask on hot paths that usually finish synchronously: a cache hit, a buffered stream read, a channel read with data waiting.IAsyncEnumerator<T>.MoveNextAsync and IAsyncDisposable.DisposeAsync return ValueTask for exactly this reason./// <summary>A cache whose hits finish synchronously, so they return ValueTask.</summary>
/// <example>new SquareCache().GetAsync(3) completes with 9</example>
public sealed class SquareCache
{
private readonly Dictionary<int, int> _cache = new();
/// <summary>How many times the slow path ran.</summary>
public int Loads { get; private set; }
/// <summary>Returns key squared. Hits allocate nothing. Misses go async.</summary>
/// <param name="key">The number to square.</param>
/// <returns>A ValueTask that is already complete on a hit.</returns>
/// <example>await cache.GetAsync(4) returns 16</example>
public ValueTask<int> GetAsync(int key)
{
// Hit: wrap the value directly. No Task object is created.
if (_cache.TryGetValue(key, out int hit)) return new ValueTask<int>(hit);
// Miss: fall back to a real async method and wrap its Task.
return new ValueTask<int>(LoadAsync(key));
}
private async Task<int> LoadAsync(int key)
{
Loads++;
// Task.Yield forces a real async hop, like an I/O call would.
await Task.Yield();
int value = key * key;
_cache[key] = value;
return value;
}
/// <summary>Reads the same key three times and reports hits and loads.</summary>
/// <returns>Sum of the three reads, how many finished synchronously, and loads.</returns>
/// <example>SquareCache.Demo() returns "sum=27 sync=2 loads=1"</example>
public static string Demo()
{
var cache = new SquareCache();
// sync counts reads that were complete the moment they returned. Starts at 0.
int sum = 0, sync = 0;
// 3 reads of key 3: the first misses, the other two hit.
for (int i = 0; i < 3; i++)
{
ValueTask<int> vt = cache.GetAsync(3);
if (vt.IsCompletedSuccessfully) sync++;
// Consume each ValueTask exactly once. Here, block for the demo.
sum += vt.AsTask().GetAwaiter().GetResult();
}
return $"sum={sum} sync={sync} loads={cache.Loads}";
}
}
ValueTask once. Do not await it twice, await it concurrently, or call .Result before it completes. A pooled source may already be reused. Call .AsTask() if you need a Task.Task to ValueTask is a breaking change, and it only pays off if most calls finish synchronously.Task.FromResult still allocates, except for a few cached values such as true, false and small ints.ValueTask<T> instead of Task<T>?Task is simpler and safer, because it can be awaited many times.Task that you must not do with a ValueTask?An async method compiles to a stub plus a state machine struct. The stub creates the struct, starts it, and returns the builder's Task. The struct's MoveNext() runs your code up to the first await on an unfinished task. There it saves its state and asks the awaiter to call MoveNext() again when done. Then it returns. This is the same shape as the iterator state machine, but driven by task completion instead of a foreach.
Reading the figure. Amber boxes are slices of your code running inside MoveNext(). Blue boxes are suspensions: the state field is saved and MoveNext() returns. Dashed arrows are the continuation firing later. Green is the end, where the builder completes the Task the caller has held since the first slice. If a were already done, the first blue box is skipped and the code runs straight on.
Knowing this answers several interview questions at once. No thread waits during an await. Locals live in the struct, which moves to the heap only on the first real suspension. An exception thrown in your code is caught by the machine and stored in the Task. A method that finishes without ever suspending runs fully synchronously.
A hand-written state machine for AddLaterAsync. The compiler's real output uses the same builder calls and the same state numbers.
using System.Runtime.CompilerServices;
public static class HandAsync
{
/// <summary>
/// Hand-written equivalent of:
/// async Task<int> AddLaterAsync(Task<int> a, Task<int> b)
/// { int x = await a; int y = await b; return x + y; }
/// </summary>
/// <param name="a">First value, maybe not ready yet.</param>
/// <param name="b">Second value, maybe not ready yet.</param>
/// <returns>A task for a + b.</returns>
/// <example>await AddLaterAsync(Task.FromResult(2), Task.FromResult(3)) returns 5</example>
public static Task<int> AddLaterAsync(Task<int> a, Task<int> b)
{
// The stub: build the machine, start it, hand back the builder's Task.
var sm = new AddMachine
{
A = a,
B = b,
Builder = AsyncTaskMethodBuilder<int>.Create(),
State = -1, // -1 = running, the compiler's code for "not suspended"
};
sm.Builder.Start(ref sm);
return sm.Builder.Task;
}
private struct AddMachine : IAsyncStateMachine
{
// -1 running, 0 suspended at await a, 1 suspended at await b, -2 finished.
public int State;
public AsyncTaskMethodBuilder<int> Builder;
public Task<int> A, B;
private int _x; // the local x, hoisted to a field
private TaskAwaiter<int> _awaiter; // the awaiter kept across a suspension
public void MoveNext()
{
int result;
try
{
TaskAwaiter<int> aw;
// Resume where we left off. 0 and 1 are the two await points.
switch (State)
{
case 0: aw = _awaiter; _awaiter = default; State = -1; goto ResumeA;
case 1: aw = _awaiter; _awaiter = default; State = -1; goto ResumeB;
}
aw = A.GetAwaiter();
if (!aw.IsCompleted)
{
State = 0; // 0 = "come back to ResumeA"
_awaiter = aw;
Builder.AwaitUnsafeOnCompleted(ref aw, ref this);
return; // the thread is free now
}
ResumeA:
_x = aw.GetResult(); // rethrows if a faulted
aw = B.GetAwaiter();
if (!aw.IsCompleted)
{
State = 1; // 1 = "come back to ResumeB"
_awaiter = aw;
Builder.AwaitUnsafeOnCompleted(ref aw, ref this);
return;
}
ResumeB:
result = _x + aw.GetResult();
}
catch (Exception ex)
{
State = -2; // -2 = finished
Builder.SetException(ex); // the exception goes into the Task
return;
}
State = -2;
Builder.SetResult(result);
}
public void SetStateMachine(IAsyncStateMachine machine) =>
Builder.SetStateMachine(machine);
}
/// <summary>Runs the hand-written machine with one value that arrives late.</summary>
/// <returns>Whether the task was done before b arrived, then the sum.</returns>
/// <example>HandAsync.Demo() returns "doneEarly=False sum=5"</example>
public static string Demo()
{
// 2 is ready now. 3 arrives later through the completion source.
var late = new TaskCompletionSource<int>();
Task<int> sum = AddLaterAsync(Task.FromResult(2), late.Task);
bool doneEarly = sum.IsCompleted;
late.SetResult(3);
return $"doneEarly={doneEarly} sum={sum.GetAwaiter().GetResult()}";
}
}
async method with no await runs fully synchronously on the caller's thread. The compiler warns with CS1998.Task.Run for CPU work, not async.async method surface when the task is awaited, not at the call. Split into a sync wrapper and an async core, as with iterators.await create a thread?By default await captures SynchronizationContext.Current. If there is none, it captures the current TaskScheduler. The continuation is then posted back there. On a UI thread that means “resume on the UI thread”, so you can touch controls after the await. ConfigureAwait(false) says “do not capture”. The continuation runs wherever the task completed, usually a thread pool thread.
ConfigureAwait(false) on every await. The library never needs the caller's UI thread, and it avoids deadlocks if a caller blocks.await: keep the default.SynchronizationContext, so it makes no difference there. Console apps have none either.ConfigureAwait(ConfigureAwaitOptions). SuppressThrowing lets you await a task without rethrowing its exception. ForceYielding always suspends, even if the task is already done.public static class ContextDemo
{
/// <summary>Awaits once and reports whether it resumed on the captured context.</summary>
/// <param name="capture">False means ConfigureAwait(false).</param>
/// <returns>True if the continuation ran on the original context.</returns>
/// <example>await ResumesOnContext(true) is true inside a SingleThreadContext</example>
public static async Task<bool> ResumesOnContext(bool capture)
{
SynchronizationContext? before = SynchronizationContext.Current;
// 5 ms makes sure the delay is unfinished at the await, so we really suspend.
await Task.Delay(5).ConfigureAwait(capture);
return before is not null && SynchronizationContext.Current == before;
}
/// <summary>Runs both variants on a UI-like context.</summary>
/// <returns>Where each variant resumed.</returns>
/// <example>ContextDemo.Demo() returns "default=True false=False"</example>
public static string Demo()
{
bool captured = SingleThreadContext.Run(() => ResumesOnContext(capture: true));
bool notCaptured = SingleThreadContext.Run(() => ResumesOnContext(capture: false));
return $"default={captured} false={notCaptured}";
}
/// <summary>Awaits a faulted task without throwing, using .NET 8 options.</summary>
/// <returns>"swallowed" plus whether the task is faulted.</returns>
/// <example>ContextDemo.SuppressDemo() returns "swallowed faulted=True"</example>
public static string SuppressDemo()
{
Task failing = Task.FromException(new InvalidOperationException("boom"));
async Task<string> Inner()
{
// SuppressThrowing waits for completion but does not rethrow the fault.
await failing.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
return $"swallowed faulted={failing.IsFaulted}";
}
return Inner().GetAwaiter().GetResult();
}
}
ConfigureAwait(false) affects only that one await. If the task is already complete, nothing changes and you stay on the current thread. Apply it to every await in a library, not just the first.ConfigureAwait(false) in UI code, touching a control throws a cross-thread exception.SuppressThrowing is for Task, not Task<T>. The compiler rejects it on a generic task in recent versions because the result would be undefined.ConfigureAwait(false) do, and do you need it in ASP.NET Core?await not to resume on the captured SynchronizationContext. ASP.NET Core has no such context, so app code there does not need it. Libraries should still use it, since they may run under WPF or WinForms.The classic deadlock needs three things. A thread with a single-threaded context, such as a UI thread, blocks on a task with .Result or .Wait(). The task's async method awaited something without ConfigureAwait(false). So its continuation is queued to that same blocked thread. The thread waits for the task. The task waits for the thread. Neither moves.
Reading the figure. Red is the blocked thread. Amber is the async method, ready to finish. Blue is the context's queue, holding the continuation. The dashed arrow is the step that never happens: the blocked thread would have to pump its own queue. Break any arrow and the cycle ends. ConfigureAwait(false) removes the “posts” arrow. await instead of .Result removes the “waits” arrow.
It shows up in WinForms, WPF, MAUI, Blazor WebAssembly and classic ASP.NET. Console apps and ASP.NET Core do not deadlock this way, but blocking there still wastes thread pool threads and can starve the pool under load. The real fix is “async all the way”: make callers async too.
The demo reproduces the deadlock on SingleThreadContext and detects it with a 50 ms timeout instead of hanging. Then it pumps the queue to release the stuck task.
public static class DeadlockDemo
{
private static async Task<int> GetDataAsync(bool capture)
{
// 5 ms keeps the delay unfinished at the await, so a continuation is needed.
await Task.Delay(5).ConfigureAwait(capture);
return 42; // 42 is an arbitrary sample payload
}
/// <summary>Blocks a UI-like thread on an async method and reports if it finished.</summary>
/// <param name="capture">True reproduces the bug. False applies ConfigureAwait(false).</param>
/// <returns>"finished" or "deadlocked", and the value once released.</returns>
/// <example>DeadlockDemo.BlockOn(true) returns "deadlocked then 42"</example>
public static string BlockOn(bool capture)
{
SynchronizationContext? previous = SynchronizationContext.Current;
var ui = new SingleThreadContext();
SynchronizationContext.SetSynchronizationContext(ui);
try
{
Task<int> task = GetDataAsync(capture);
// Block like .Result would, but give up after 50 ms instead of forever.
bool finished = task.Wait(TimeSpan.FromMilliseconds(50));
if (finished) return $"finished {task.Result}";
// Rescue: pump the queue so the posted continuation can run.
while (!task.IsCompleted) ui.RunPending();
return $"deadlocked then {task.Result}";
}
finally
{
SynchronizationContext.SetSynchronizationContext(previous);
}
}
}
Task.Run(() => FooAsync()).Result avoids the deadlock because the pool has no context. It still blocks a thread. Treat it as a last resort at an app boundary.ConfigureAwait(false) deep inside a library is enough to deadlock a blocking caller..GetAwaiter().GetResult() deadlocks exactly like .Result. It only changes how exceptions are wrapped..Result hang a WPF app but not a console app?SynchronizationContext. The awaited continuation is posted back to the UI thread, which is blocked on .Result. A console app has no context, so the continuation runs on the thread pool and completes the task.async event handler. In library code, also add ConfigureAwait(false) to every await.An async void method returns nothing the caller can await. The caller cannot know when it finishes. Worse, an exception cannot reach the caller. It is raised on the captured SynchronizationContext, or on the thread pool if there is none. On the thread pool, an unhandled exception crashes the process.
Use it only for event handlers, where the signature must be void. Put a try/catch around the whole body. Everywhere else return Task. Watch for lambdas too. A lambda passed to an Action parameter becomes async void silently.
public static class AsyncVoidDemo
{
private static async void FireAndForget(List<string> log)
{
await Task.Yield(); // the rest runs later, after the caller moved on
log.Add("void body ended");
}
private static async void Explode()
{
await Task.Yield();
throw new InvalidOperationException("from async void");
}
/// <summary>Shows that the caller of async void cannot wait for it.</summary>
/// <returns>The log, in the order things really happened.</returns>
/// <example>AsyncVoidDemo.CannotAwait() returns [caller done, void body ended]</example>
public static List<string> CannotAwait()
{
var log = new List<string>();
SingleThreadContext.Run(() =>
{
FireAndForget(log);
log.Add("caller done"); // runs first: nothing to await
return Task.FromResult(0);
});
return log;
}
/// <summary>Shows that the caller's try/catch never sees an async void exception.</summary>
/// <returns>What the caller caught, and what surfaced on the context.</returns>
/// <example>AsyncVoidDemo.CannotCatch() returns "caller=nothing context=from async void"</example>
public static string CannotCatch()
{
string caller = "nothing", context = "nothing";
try
{
SingleThreadContext.Run(() =>
{
try { Explode(); }
catch (InvalidOperationException) { caller = "caught"; } // never runs
return Task.FromResult(0);
});
}
catch (InvalidOperationException ex)
{
// The exception was posted to the context and thrown by its pump.
context = ex.Message;
}
return $"caller={caller} context={context}";
}
}
list.ForEach(async x => await SaveAsync(x)) compiles to async void lambdas. It returns at once and loses exceptions. Use a foreach with await, or Task.WhenAll.async void methods pass before the work even runs.Task, keep it, and observe its exception somewhere, for example with a logging continuation.async void dangerous?SynchronizationContext or crashes the process on the thread pool. Use it only for event handlers, wrapped in try/catch.Cancellation in .NET is cooperative. A CancellationTokenSource owns the switch. A CancellationToken is the read-only view you pass down. Code checks the token, or passes it to APIs that check it. On cancel, they throw OperationCanceledException. A linked source cancels when any of its parents cancel. CancelAfter turns a source into a timeout.
CancellationToken as its last parameter, defaulting to default.CreateLinkedTokenSource. Then check which one fired to report the right error.task.WaitAsync(timeout) stops waiting for a task that cannot be cancelled. The task itself keeps running.public static class CancelDemo
{
/// <summary>Sums items, checking the token before each one.</summary>
/// <param name="items">Numbers to add.</param>
/// <param name="ct">Stops the work when cancelled.</param>
/// <returns>The sum.</returns>
/// <example>await SumAsync([1, 2, 3], default) returns 6</example>
public static async Task<int> SumAsync(IEnumerable<int> items, CancellationToken ct = default)
{
// sum of the items seen so far. 0 is the empty sum.
int sum = 0;
foreach (int x in items)
{
// Throws OperationCanceledException as soon as cancel was requested.
ct.ThrowIfCancellationRequested();
sum += x;
await Task.Yield(); // a stand-in for real async work per item
}
return sum;
}
/// <summary>Cancels in the middle of a sum from a token callback.</summary>
/// <returns>The exception type that ended the work.</returns>
/// <example>CancelDemo.CancelMidway() returns "OperationCanceledException"</example>
public static string CancelMidway()
{
using var cts = new CancellationTokenSource();
// Cancel when the sequence reaches item 3. 1..10 is a sample range.
IEnumerable<int> Items()
{
for (int i = 1; i <= 10; i++)
{
if (i == 3) cts.Cancel();
yield return i;
}
}
try { SumAsync(Items(), cts.Token).GetAwaiter().GetResult(); return "finished"; }
catch (OperationCanceledException ex) { return ex.GetType().Name; }
}
/// <summary>Links a caller token with a 20 ms timeout and reports which one fired.</summary>
/// <param name="callerCancelsFirst">True cancels the caller token at once.</param>
/// <returns>"caller" or "timeout".</returns>
/// <example>CancelDemo.WhoCancelled(false) returns "timeout"</example>
public static string WhoCancelled(bool callerCancelsFirst)
{
using var caller = new CancellationTokenSource();
using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(20));
// linked fires when either parent fires.
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
caller.Token, timeout.Token);
if (callerCancelsFirst) caller.Cancel();
try
{
// Infinite delay: only cancellation can end it.
Task.Delay(Timeout.Infinite, linked.Token).GetAwaiter().GetResult();
return "impossible";
}
catch (OperationCanceledException)
{
// Ask the parents, not the linked source, to learn the cause.
return caller.IsCancellationRequested ? "caller" : "timeout";
}
}
/// <summary>Stops waiting for a task that ignores cancellation, after 20 ms.</summary>
/// <returns>The exception type, and whether the task is still running.</returns>
/// <example>CancelDemo.WaitAsyncTimeout() returns "TimeoutException stillRunning=True"</example>
public static string WaitAsyncTimeout()
{
// A task that never completes on its own and takes no token.
var never = new TaskCompletionSource<int>();
try
{
never.Task.WaitAsync(TimeSpan.FromMilliseconds(20)).GetAwaiter().GetResult();
return "finished";
}
catch (TimeoutException ex)
{
return $"{ex.GetType().Name} stillRunning={!never.Task.IsCompleted}";
}
}
}
OperationCanceledException, not TaskCanceledException. The second derives from the first, and many APIs throw only the base type.CancellationTokenSource objects, especially linked ones and ones with CancelAfter. They hold timers and registrations.ct.Register(callback) runs the callback synchronously inside Cancel(). A slow or throwing callback hurts whoever cancels.OperationCanceledException and returning a partial result hides the cancel from callers. Let it propagate unless you are at the top.CancelAfter(timeout) on it, and pass the linked token down. In the catch, check the caller's token to tell a user cancel from a timeout.Task.WhenAll completes when every task completes. It returns all results in input order, not finish order.Task.WhenAny completes when the first task completes. It returns that task, which may be faulted. It does not cancel the others.Task.WhenEach (.NET 9) is an IAsyncEnumerable that yields each task as it finishes. It replaces the old “WhenAny in a loop”, which is O(n²).Start independent work first, then await them together. Three calls of 100 ms each take about 100 ms with WhenAll and 300 ms awaited one by one. Use WhenAny for first-wins races and hand-made timeouts. Use WhenEach to process results as they arrive.
The completion sources let the test choose the finish order, so the results never depend on timing.
public static class CombinatorDemo
{
/// <summary>Finishes three tasks out of order and collects WhenAll results.</summary>
/// <returns>Results in input order, regardless of finish order.</returns>
/// <example>CombinatorDemo.AllInInputOrder() returns [1, 2, 3]</example>
public static int[] AllInInputOrder()
{
var a = new TaskCompletionSource<int>();
var b = new TaskCompletionSource<int>();
var c = new TaskCompletionSource<int>();
Task<int[]> all = Task.WhenAll(a.Task, b.Task, c.Task);
// Finish order 3, 1, 2. The values are just labels.
c.SetResult(3);
a.SetResult(1);
b.SetResult(2);
return all.GetAwaiter().GetResult();
}
/// <summary>A first-wins race with a fallback.</summary>
/// <returns>The winner's value.</returns>
/// <example>CombinatorDemo.FirstWins() returns "mirror"</example>
public static string FirstWins()
{
var primary = new TaskCompletionSource<string>();
var mirror = new TaskCompletionSource<string>();
Task<Task<string>> race = Task.WhenAny(primary.Task, mirror.Task);
mirror.SetResult("mirror");
// WhenAny returns the winning task. Await it again to get its value or exception.
Task<string> winner = race.GetAwaiter().GetResult();
primary.SetResult("primary"); // the loser still runs to the end
return winner.GetAwaiter().GetResult();
}
/// <summary>Processes results in completion order with Task.WhenEach (.NET 9).</summary>
/// <returns>Values in the order the tasks finished.</returns>
/// <example>CombinatorDemo.EachAsItFinishes() returns [20, 30, 10]</example>
public static List<int> EachAsItFinishes()
{
// 10, 20 and 30 are labels for three requests.
var r10 = new TaskCompletionSource<int>();
var r20 = new TaskCompletionSource<int>();
var r30 = new TaskCompletionSource<int>();
var order = new List<int>();
async Task Consume()
{
// Each pass gets the next task to finish. It is already complete.
await foreach (Task<int> done in Task.WhenEach(r10.Task, r20.Task, r30.Task))
order.Add(await done);
}
Task loop = Consume();
r20.SetResult(20);
r30.SetResult(30);
r10.SetResult(10);
loop.GetAwaiter().GetResult();
return order;
}
/// <summary>Runs n fake calls together and reports total work started.</summary>
/// <param name="n">How many calls.</param>
/// <returns>The sum of the call results.</returns>
/// <example>CombinatorDemo.StartThenAwait(4) returns 10</example>
public static int StartThenAwait(int n)
{
static async Task<int> FakeCall(int id)
{
await Task.Delay(10); // 10 ms: each call alone is short
return id;
}
// Start all calls first. Ids run 1..n so the sum is n * (n + 1) / 2.
Task<int>[] calls = Enumerable.Range(1, n).Select(FakeCall).ToArray();
return Task.WhenAll(calls).GetAwaiter().GetResult().Sum();
}
}
await Task.WhenAll(...) rethrows only the first exception. See the exceptions section to get all of them.WhenAny never throws for a faulted winner. You must await the returned task to see the error.ids.Select(FetchAsync) is lazy. Enumerating it twice starts every call twice. Call ToArray() before WhenAll if you reuse it.WhenAll over thousands of calls fires them all at once. Throttle with Parallel.ForEachAsync or a SemaphoreSlim.Task.WhenEach give you over a WhenAny loop?WhenAny again after each finish, so it costs O(n²). WhenEach registers once per task and yields tasks as they complete. It is O(n) and reads as a plain await foreach.TaskCompletionSource<T> is a task you complete by hand. You hand out tcs.Task. Later you call SetResult, SetException or SetCanceled, or the Try versions. It is the bridge from callbacks and events into await.
/// <summary>An old-style API that reports results through events.</summary>
/// <example>var d = new LegacyDownloader(); d.Start("a"); d.Finish(); // raises Completed</example>
public sealed class LegacyDownloader
{
private string _url = "";
/// <summary>Raised with the content when a download finishes.</summary>
public event Action<string>? Completed;
/// <summary>Raised when a download fails.</summary>
public event Action<Exception>? Failed;
/// <summary>Begins a download. The result arrives later via an event.</summary>
public void Start(string url) => _url = url;
/// <summary>Simulates the download succeeding.</summary>
public void Finish() => Completed?.Invoke($"content of {_url}");
/// <summary>Simulates the download failing.</summary>
public void Fail() => Failed?.Invoke(new IOException($"cannot reach {_url}"));
}
public static class TcsDemo
{
/// <summary>Wraps the event API in a Task.</summary>
/// <param name="d">The downloader.</param>
/// <param name="url">What to fetch.</param>
/// <returns>A task that completes when an event fires.</returns>
/// <example>await DownloadAsync(d, "x") returns "content of x" once d finishes</example>
public static Task<string> DownloadAsync(LegacyDownloader d, string url)
{
// RunContinuationsAsynchronously stops awaiters from running inside our event call.
var tcs = new TaskCompletionSource<string>(
TaskCreationOptions.RunContinuationsAsynchronously);
Action<string>? onDone = null;
Action<Exception>? onFail = null;
// Unhook both handlers when either fires, so nothing leaks.
onDone = s => { Unhook(); tcs.TrySetResult(s); };
onFail = e => { Unhook(); tcs.TrySetException(e); };
void Unhook() { d.Completed -= onDone; d.Failed -= onFail; }
d.Completed += onDone;
d.Failed += onFail;
d.Start(url);
return tcs.Task;
}
/// <summary>Shows a success and a failure through the wrapper.</summary>
/// <returns>The content, then the failure type.</returns>
/// <example>TcsDemo.Demo() returns "content of a.txt | IOException"</example>
public static string Demo()
{
var ok = new LegacyDownloader();
Task<string> good = DownloadAsync(ok, "a.txt");
ok.Finish();
var bad = new LegacyDownloader();
Task<string> broken = DownloadAsync(bad, "b.txt");
bad.Fail();
string failure;
try { broken.GetAwaiter().GetResult(); failure = "none"; }
catch (IOException ex) { failure = ex.GetType().Name; }
return $"{good.GetAwaiter().GetResult()} | {failure}";
}
/// <summary>Shows that SetResult twice throws, while TrySetResult returns false.</summary>
/// <returns>The result of the second TrySetResult.</returns>
/// <example>TcsDemo.SecondTry() returns false</example>
public static bool SecondTry()
{
var tcs = new TaskCompletionSource();
tcs.SetResult();
return tcs.TrySetResult();
}
}
RunContinuationsAsynchronously, SetResult runs awaiting code inline on your thread. That can run user code while you hold a lock, or recurse deeply.SetResult on an already completed source throws InvalidOperationException. Use the TrySet methods when two paths race to finish it.TaskCompletionSource<T>. Subscribe handlers that call TrySetResult or TrySetException and unsubscribe. Start the operation and return tcs.Task. Pass RunContinuationsAsynchronously.An async iterator is a method with async, yield return and a return type of IAsyncEnumerable<T>. It can await between items. Consumers read it with await foreach. Each step calls MoveNextAsync(), which returns a ValueTask<bool>. .NET 10 ships LINQ operators for it in the box: Where, Select, ToListAsync and more.
Use it for data that arrives over time: paged APIs, database rows, lines from a socket. The consumer starts on page one while page two is still loading. Memory stays flat, unlike Task<List<T>>, which waits for everything.
public static class AsyncStreams
{
/// <summary>Yields items page by page, awaiting a fake fetch for each page.</summary>
/// <param name="pages">How many pages.</param>
/// <param name="pageSize">Items per page.</param>
/// <param name="ct">Set by WithCancellation thanks to [EnumeratorCancellation].</param>
/// <returns>Item numbers 0 .. pages * pageSize - 1.</returns>
/// <example>FetchAllAsync(2, 2) yields 0, 1, 2, 3</example>
public static async IAsyncEnumerable<int> FetchAllAsync(
int pages, int pageSize,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
{
// page is the page being fetched. Pages are numbered from 0.
for (int page = 0; page < pages; page++)
{
await Task.Yield(); // stands in for one network call per page
ct.ThrowIfCancellationRequested();
// i is the slot within the page, 0 .. pageSize - 1.
for (int i = 0; i < pageSize; i++)
yield return page * pageSize + i;
}
}
/// <summary>Reads a whole async stream with await foreach.</summary>
/// <returns>All items from 3 pages of 2.</returns>
/// <example>AsyncStreams.ReadAll() returns [0, 1, 2, 3, 4, 5]</example>
public static List<int> ReadAll()
{
async Task<List<int>> Go()
{
var all = new List<int>();
// 3 pages of 2 items is a small sample.
await foreach (int x in FetchAllAsync(3, 2)) all.Add(x);
return all;
}
return Go().GetAwaiter().GetResult();
}
/// <summary>Uses the built-in async LINQ in .NET 10 to filter a stream.</summary>
/// <returns>Even items from 3 pages of 3.</returns>
/// <example>AsyncStreams.EvensWithLinq() returns [0, 2, 4, 6, 8]</example>
public static List<int> EvensWithLinq() =>
FetchAllAsync(3, 3) // 3 pages of 3: items 0..8
.Where(x => x % 2 == 0) // % 2 == 0 keeps even items
.ToListAsync()
.AsTask().GetAwaiter().GetResult();
/// <summary>Cancels a stream after the first page using WithCancellation.</summary>
/// <returns>Items read before the cancel, then the exception type.</returns>
/// <example>AsyncStreams.CancelAfterFirstPage() returns "read=2 OperationCanceledException"</example>
public static string CancelAfterFirstPage()
{
async Task<string> Go()
{
using var cts = new CancellationTokenSource();
int read = 0;
try
{
// 5 pages of 2. The token reaches the iterator through WithCancellation.
await foreach (int x in FetchAllAsync(5, 2).WithCancellation(cts.Token))
{
read++;
if (read == 2) cts.Cancel(); // 2 = the end of the first page
}
return "finished";
}
catch (OperationCanceledException ex) { return $"read={read} {ex.GetType().Name}"; }
}
return Go().GetAwaiter().GetResult();
}
}
[EnumeratorCancellation] on the token parameter, WithCancellation has nowhere to put its token. The compiler warns.await foreach is sequential. It does not fetch the next item while you process the current one. Use a channel for real overlap.System.Linq.Async NuGet package. Having both can cause ambiguous calls.Task<List<T>> or IAsyncEnumerable<T>?Task<List<T>> when the set is small and you need it all at once. IAsyncEnumerable<T> when data arrives in pieces or is large. The consumer can start early and stop early, and memory stays bounded.IAsyncDisposable has one method, ValueTask DisposeAsync(). await using calls it at the end of the scope, even after an exception. It exists because some cleanup must do I/O. A stream may need to flush a buffer, or a connection may need to send a goodbye message. Blocking on that inside Dispose() would be sync-over-async.
Implement it on types whose cleanup is naturally async. Also keep IDisposable if sync callers exist. Use await using on FileStream, DbConnection, Utf8JsonWriter and similar types.
/// <summary>A writer that buffers lines and flushes them asynchronously on dispose.</summary>
/// <example>await using (var w = new BufferedSink("a", log)) w.Write("x");</example>
public sealed class BufferedSink(string name, List<string> log) : IAsyncDisposable
{
private readonly List<string> _buffer = new();
/// <summary>Buffers a line. Nothing is written yet.</summary>
public void Write(string line) => _buffer.Add(line);
/// <summary>Flushes the buffer with an async call, then records the disposal.</summary>
public async ValueTask DisposeAsync()
{
await Task.Yield(); // stands in for an async flush to disk or network
log.Add($"{name} flushed {_buffer.Count}");
}
}
public static class AsyncDisposeDemo
{
/// <summary>Two await using declarations dispose in reverse order, even on error.</summary>
/// <param name="fail">Throw inside the scope.</param>
/// <returns>The disposal log.</returns>
/// <example>AsyncDisposeDemo.Run(false) returns [second flushed 1, first flushed 2]</example>
public static List<string> Run(bool fail)
{
var log = new List<string>();
async Task Body()
{
await using var first = new BufferedSink("first", log);
await using var second = new BufferedSink("second", log);
first.Write("a");
first.Write("b");
second.Write("c");
if (fail) throw new InvalidOperationException("mid-scope failure");
}
try { Body().GetAwaiter().GetResult(); }
catch (InvalidOperationException) { log.Add("caught"); }
return log;
}
}
using on a type that only implements IAsyncDisposable does not compile. A type with both runs the sync Dispose, which may skip the async flush.ConfigureAwait(false), write await using (x.ConfigureAwait(false)), or declare the configured result with await using var.DisposeAsync should be safe to call twice, just like Dispose.IAsyncDisposable exist when we already have IDisposable?Dispose() means blocking on async work. DisposeAsync lets await using release the thread during cleanup.A Channel<T> is an async queue with a writer end and a reader end. Channel.CreateBounded<T>(capacity) limits its size. When it is full, WriteAsync waits, which is back-pressure. Readers await foreach over reader.ReadAllAsync(). The loop ends after the writer calls Complete() and the queue drains.
Reading the figure. Amber is the producer, blue is the channel, green are consumers. The two slots are full, so the producer's next WriteAsync waits without blocking a thread. Each item goes to exactly one consumer.
Use a channel for pipelines: read, then parse, then save, with each stage at its own speed. Use it for background work queues in a service, and for fan-out to N workers. It replaces BlockingCollection<T>, which blocks threads.
using System.Threading.Channels;
public static class ChannelDemo
{
/// <summary>One producer writes 1..n into a bounded channel. Two consumers sum it.</summary>
/// <param name="n">How many items to produce.</param>
/// <returns>Total of both consumers, and how many items each one handled together.</returns>
/// <example>ChannelDemo.Pipeline(100) returns "sum=5050 items=100"</example>
public static string Pipeline(int n)
{
// Capacity 2 keeps the queue tiny, so back-pressure really happens.
var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(2)
{
FullMode = BoundedChannelFullMode.Wait, // writers wait when full
});
async Task Produce()
{
// i is the next item. 1..n so the expected sum is n * (n + 1) / 2.
for (int i = 1; i <= n; i++)
await channel.Writer.WriteAsync(i);
// Tell readers no more items are coming.
channel.Writer.Complete();
}
async Task<(int Sum, int Count)> Consume()
{
int sum = 0, count = 0; // 0: this consumer has seen nothing yet
await foreach (int x in channel.Reader.ReadAllAsync())
{
sum += x;
count++;
}
return (sum, count);
}
// Start both consumers on the pool, then produce.
Task<(int Sum, int Count)> a = Task.Run(Consume);
Task<(int Sum, int Count)> b = Task.Run(Consume);
Task.WhenAll(Produce(), a, b).GetAwaiter().GetResult();
return $"sum={a.Result.Sum + b.Result.Sum} items={a.Result.Count + b.Result.Count}";
}
/// <summary>Shows TryWrite failing on a full bounded channel.</summary>
/// <returns>The result of three TryWrite calls on a channel of capacity 2.</returns>
/// <example>ChannelDemo.TryWriteWhenFull() returns [true, true, false]</example>
public static List<bool> TryWriteWhenFull()
{
var ch = Channel.CreateBounded<int>(2); // 2 slots
// Items 1, 2, 3: the third has no room.
return [ch.Writer.TryWrite(1), ch.Writer.TryWrite(2), ch.Writer.TryWrite(3)];
}
}
Writer.Complete() leaves every reader waiting forever.DropOldest, DropNewest and DropWrite silently lose data. Use them only for telemetry-style streams.Complete(exception) when the producer fails, so readers see the error instead of a clean end.Channel<T>. Request handlers call WriteAsync. A hosted service loops with await foreach over ReadAllAsync(stoppingToken). The bound gives back-pressure, and nothing blocks a thread.Parallel.ForEachAsync runs an async body for each item, with at most MaxDegreeOfParallelism bodies in flight. It accepts both IEnumerable<T> and IAsyncEnumerable<T>. It passes a token to each body and stops starting new items after the first failure or a cancel.
Use it for throttled I/O fan-out, such as “call this API for 10,000 ids, at most 8 at a time”. Before .NET 6 that took a SemaphoreSlim plus WhenAll. Do not use Parallel.ForEach with an async lambda. That creates async void bodies that the loop does not wait for.
public static class ParallelDemo
{
/// <summary>Processes n items with a cap of 3 in flight and records the peak.</summary>
/// <param name="n">How many items.</param>
/// <returns>The sum of item squares, and whether the peak stayed within the cap.</returns>
/// <example>ParallelDemo.Throttled(10) returns "sum=385 peakOk=True"</example>
public static string Throttled(int n)
{
// 3 is the cap: at most three bodies run at the same moment.
const int Cap = 3;
int inFlight = 0, peak = 0;
long sum = 0;
var options = new ParallelOptions { MaxDegreeOfParallelism = Cap };
Parallel.ForEachAsync(Enumerable.Range(1, n), options, async (item, ct) =>
{
// Track the live count with Interlocked, since bodies run on many threads.
int now = Interlocked.Increment(ref inFlight);
// Raise peak to now, retrying if another thread changed it in between.
int seen;
while ((seen = Volatile.Read(ref peak)) < now &&
Interlocked.CompareExchange(ref peak, now, seen) != seen) { }
await Task.Delay(2, ct); // 2 ms of fake I/O
Interlocked.Add(ref sum, (long)item * item);
Interlocked.Decrement(ref inFlight);
}).GetAwaiter().GetResult();
return $"sum={sum} peakOk={peak <= Cap}";
}
}
Environment.ProcessorCount. That suits CPU work, but is often too low or too high for I/O. Set it on purpose.Interlocked, a lock or a concurrent collection.Parallel.ForEachAsync(ids, new ParallelOptions { MaxDegreeOfParallelism = 10 }, async (id, ct) => ...). On older .NET, use a SemaphoreSlim(10) with WaitAsync and Release around each call, plus Task.WhenAll.A faulted Task stores an AggregateException in task.Exception. How you observe it decides what you see.
await and GetAwaiter().GetResult() rethrow the first inner exception, with its original stack trace..Result and .Wait() throw the AggregateException wrapper.await Task.WhenAll(...) throws only the first failure. The rest are in whenAllTask.Exception.InnerExceptions.Interviewers ask “three tasks fail in a WhenAll, what do you catch?”. Keep the WhenAll task in a variable. Then inspect its Exception in the catch block to log every failure.
public static class AsyncErrors
{
private static async Task FailAsync(string message)
{
await Task.Yield();
throw new InvalidOperationException(message);
}
/// <summary>Compares the exception type seen by await, GetResult and Wait.</summary>
/// <returns>The type each style throws.</returns>
/// <example>AsyncErrors.WhatYouCatch() returns "await=InvalidOperationException ..."</example>
public static string WhatYouCatch()
{
string viaAwait = "", viaGetResult = "", viaWait = "";
async Task Awaiter()
{
try { await FailAsync("x"); }
catch (Exception ex) { viaAwait = ex.GetType().Name; }
}
Awaiter().GetAwaiter().GetResult();
try { FailAsync("x").GetAwaiter().GetResult(); }
catch (Exception ex) { viaGetResult = ex.GetType().Name; }
try { FailAsync("x").Wait(); }
catch (Exception ex) { viaWait = ex.GetType().Name; }
return $"await={viaAwait} getresult={viaGetResult} wait={viaWait}";
}
/// <summary>Three tasks fail in WhenAll. Await sees one. The task holds all three.</summary>
/// <returns>The caught message, then every message from the WhenAll task.</returns>
/// <example>AsyncErrors.AllFailures() returns "caught=a all=a,b,c"</example>
public static string AllFailures()
{
async Task<string> Go()
{
// Already-faulted tasks keep the order fixed: a, b, c.
// With real async work, the order follows completion instead.
static Task Failed(string m) => Task.FromException(new InvalidOperationException(m));
// Keep the WhenAll task so its full exception list stays reachable.
Task all = Task.WhenAll(Failed("a"), Failed("b"), Failed("c"));
try
{
await all;
return "no error";
}
catch (InvalidOperationException first)
{
IEnumerable<string> every =
all.Exception!.InnerExceptions.Select(e => e.Message);
return $"caught={first.Message} all={string.Join(",", every)}";
}
}
return Go().GetAwaiter().GetResult();
}
/// <summary>Shows that an exception thrown before the first await still lands in the Task.</summary>
/// <returns>Whether calling threw, and whether the returned task is faulted.</returns>
/// <example>AsyncErrors.ThrowBeforeAwait() returns "callThrew=False faulted=True"</example>
public static string ThrowBeforeAwait()
{
static async Task Validate(int x)
{
// Below 0 is invalid. This throw happens before any await runs.
if (x < 0) throw new ArgumentOutOfRangeException(nameof(x));
await Task.Yield();
}
bool callThrew = false;
Task t = Task.CompletedTask;
// -1 is a sample invalid input.
try { t = Validate(-1); }
catch (ArgumentOutOfRangeException) { callThrew = true; }
return $"callThrew={callThrew} faulted={t.IsFaulted}";
}
}
catch (AggregateException) around an await never matches. await unwraps it.TaskScheduler.UnobservedTaskException when collected. Since .NET 4.5 that no longer crashes the process, so failures vanish quietly.async methods do not throw at the call. They throw at the await. Validation that must fail fast needs a sync wrapper.Task.WhenAll throw. What does await throw, and how do you see all three?await throws the first exception, unwrapped. Keep the WhenAll task in a variable and read task.Exception.InnerExceptions in the catch block..Result and .GetAwaiter().GetResult()?.Result wraps failures in AggregateException. GetResult() rethrows the original exception, like await. Neither avoids the deadlock.async compiles to a state machine. Each await on an unfinished task ends a slice. No thread waits.Task by default. Use ValueTask for hot paths that usually finish synchronously, and consume it once..Result on a single-threaded context deadlocks. Await all the way, and add ConfigureAwait(false) in libraries.async void cannot be awaited or caught. Use it only for event handlers.WhenAll for all, WhenAny for a race, WhenEach (.NET 9) to handle results as they finish.TaskCompletionSource bridges events into tasks. Channels give async producer and consumer queues with back-pressure.await throws the first exception. The task's Exception holds them all.Async code passes results through many layers, and those results often have a shape worth matching on. A 06, Pattern Matching and Records, covers the C# features for taking data apart and building immutable values.