C# async exception handling: await, Task.WhenAll AggregateException and fire-and-forget tasks

C# Async & Multithreading November 13, 2026 csharp

await rethrows the first exception with its original stack trace, not an AggregateException.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ======================================================================
// Q: How are exceptions handled with await, Task.WhenAll and fire-and-forget?
//
// - await rethrows the first exception with its original stack trace, not an
//   AggregateException. .Result / .Wait() throw AggregateException.
// - Task.WhenAll: if several tasks fail, await throws only the first one. All of
//   them are in the task's Exception.InnerExceptions, as shown in the code.
// - A Task that fails and is never awaited: the exception is silently lost
//   (TaskScheduler.UnobservedTaskException). That is why you don't leave
//   "orphan" tasks.
// - An exception escaping BackgroundService.ExecuteAsync: since .NET 6 it stops
//   the whole host (BackgroundServiceExceptionBehavior.StopHost). That is why
//   you put a try/catch inside the loop (as in the demo's OutboxProcessor).
// ======================================================================

// Log every failure from Task.WhenAll, then rethrow
var all = Task.WhenAll(tasks);
try { await all; }
catch
{
    foreach (var ex in all.Exception!.InnerExceptions)
        logger.LogError(ex, "Price sync failed");
    throw;
}