await rethrows the first exception with its original stack trace, not an AggregateException.
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; } |