1. Вступ
Уявіть собі: ви запускаєте одразу з десяток асинхронних завдань — парсите вебсайти, перевіряєте ціни на маркетплейсах або рахуєте великі масиви даних. Усе жваво стартує, але кілька завдань завершуються з помилками: десь сервер не відповідає, десь помилка в даних. У синхронному коді ви б побачили один виняток, а в асинхронному — їх може бути кілька. Тут на сцену виходить AggregateException, який збирає всі помилки в один «контейнер».
Що таке AggregateException?
AggregateException — спеціальний тип винятку в .NET, що агрегує помилки з паралельних/асинхронних операцій: Task.WhenAll, Parallel.For, TPL тощо. Коли дві й більше завдань завершуються з помилкою, вони «пакуються» в один об’єкт AggregateException.
Де виникає AggregateException?
Найчастіший сценарій — очікування кількох завдань через Task.WhenAll:
// Приклад — паралельно викликаємо кілька завдань, частина з яких викидає виняток
var tasks = new List<Task>
{
Task.Run(() => throw new InvalidOperationException("Помилка #1")),
Task.Run(() => throw new ArgumentException("Помилка #2")),
Task.Run(() => { Console.WriteLine("Завдання 3 виконалося успішно!"); })
};
try
{
await Task.WhenAll(tasks); // При await у цьому місці AggregateException розгортається
}
catch (Exception ex)
{
Console.WriteLine($"Сталася помилка: {ex.GetType().Name} — {ex.Message}");
}
Втім, є нюанс: під час await AggregateException розгортається, і викидається перший внутрішній виняток «як є». Якщо ж не використовувати await, а звернутися до Result або викликати Wait(), ви отримаєте саме AggregateException.
2. Як виглядає AggregateException?
Це виняток із колекцією InnerExceptions — усіма вкладеними помилками:
try
{
Task task = Task.WhenAll(tasks);
task.Wait(); // Тут буде саме AggregateException, якщо були помилки
}
catch (AggregateException aggEx)
{
Console.WriteLine($"Усього помилок: {aggEx.InnerExceptions.Count}");
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"Тип: {e.GetType().Name}, Повідомлення: {e.Message}");
}
}
Приклад виведення:
Усього помилок: 2
Тип: InvalidOperationException, Повідомлення: Помилка #1
Тип: ArgumentException, Повідомлення: Помилка #2
Як виникає AggregateException?
+--------------------------+
| Запускаємо кілька |
| Task'ів |
+-----+---------+----------+
| |
v v
Task1 Task2 ... TaskN
| |
| (падає з помилкою)
|-------------------+
| v
| Exception2
v
Exception1
|
v
+---------------------------------+
| Task.WhenAll або Parallel.For |
| (збирає всі |
| винятки) |
+---------------------------------+
|
v
AggregateException
(InnerExceptions: усі помилки)
Чому з await — не видно AggregateException?
Під час await .NET «розгортає» AggregateException і викидає перший внутрішній виняток (InnerExceptions[0]).
// Лише одне завдання, яке викидає InvalidOperationException
try
{
await Task.Run(() => throw new InvalidOperationException("Помилка!"));
}
catch (Exception ex)
{
// Тут ex — саме InvalidOperationException, а не AggregateException
Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}
Хочете отримати доступ до усіх помилок — використовуйте Wait() або Result (але пам’ятайте про блокування потоку).
3. Ловимо й обробляємо всі винятки
Пов’яжімо теорію з практикою. Припустімо, кілька завдань обробляють дані користувачів, і вам потрібно зафіксувати всі збої.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApp
{
class Program
{
static async Task Main(string[] args)
{
var tasks = new List<Task>
{
Task.Run(() => throw new InvalidOperationException("Не та операція!")),
Task.Run(() => throw new DivideByZeroException("Ділення на нуль!")),
Task.Run(() => Console.WriteLine("Третє завдання пройшло успішно"))
};
try
{
// Очікуємо завдання асинхронно — отримуємо перший внутрішній виняток
Task allTasks = Task.WhenAll(tasks);
await allTasks; // під час await AggregateException розгортається
}
catch (Exception ex)
{
// Під час await бачитимемо перший внутрішній виняток
Console.WriteLine($"Перша помилка: {ex.GetType().Name} — {ex.Message}");
// Якщо це AggregateException — можна пройтися всіма
if (ex is AggregateException aggEx)
{
foreach (var inner in aggEx.InnerExceptions)
{
Console.WriteLine($"Помилка зі списку: {inner.GetType().Name} — {inner.Message}");
}
}
}
// Тепер ловимо всі помилки синхронно
try
{
var allTasks = Task.WhenAll(tasks);
allTasks.Wait(); // блокуємо потік — так не робіть в UI!
}
catch (AggregateException aggEx)
{
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"[Wait] Помилка: {e.GetType().Name} — {e.Message}");
}
}
}
}
}
Під час await у блоці try/catch ви побачите лише першу помилку. При Wait() ви отримаєте класичний AggregateException з усіма вкладеними помилками.
4. Обробка масових помилок
У реальних застосунках часто потрібно обробляти багато помилок різними способами. Наприклад, ви надсилаєте електронні листи всім користувачам, і частина відправок падає:
var sendTasks = emailList.Select(email => Task.Run(() => SendEmail(email))).ToList();
try
{
Task.WaitAll(sendTasks.ToArray());
}
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.InnerExceptions)
{
if (ex is SmtpException)
Console.WriteLine("Помилка надсилання листа: " + ex.Message);
else
Console.WriteLine("Невідома помилка: " + ex.Message);
}
}
Так ви фіксуєте кожен збій і можете вжити індивідуальних заходів.
5. Корисні нюанси
Причини такої поведінки
.NET орієнтується на зручність: в UI зазвичай достатньо першої помилки (під час await). Але на сервері чи в системах, де важливо обробити всі невдачі, незамінний AggregateException.
Патерн: Flatten — розгорнути список помилок
Метод Flatten() перетворює вкладені AggregateException на плоский список:
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.Flatten().InnerExceptions)
{
Console.WriteLine($"Розгорнута помилка: {ex.GetType().Name} — {ex.Message}");
}
}
Яка поведінка у Task при різних способах очікування
| Як очікуємо завдання | Який виняток спіймаємо |
|---|---|
|
Перший внутрішній виняток |
|
Завжди AggregateException |
|
Завжди AggregateException |
| Окремий await | Виняток, викинутий завданням, або перший внутрішній виняток, якщо завдання завершилося з AggregateException |
Важливо пам’ятати
- AggregateException корисний у всіх сценаріях, де помилок може бути багато водночас.
- Для await зручно ловити першу помилку; для синхронного очікування — обходити всі InnerExceptions.
- Використовуйте Flatten() і Handle() для акуратної обробки.
- Не змішуйте синхронні й асинхронні очікування в UI: отримаєте зависання та дедлоки.
7. Типові помилки під час обробки AggregateException
Міф: await Task.WhenAll завжди викидає AggregateException. Насправді — ні, виняток розгортається, і ви побачите перший внутрішній.
Звернення до Result або виклик Wait() блокує потік до завершення завдань. В UI це призведе до зависань інтерфейсу.
Якщо хочете обробити частину помилок «тихо», використовуйте Handle():
catch (AggregateException aggEx)
{
aggEx.Handle(ex =>
{
if (ex is InvalidOperationException)
{
// Цю помилку ми обробили, можна не викидати далі
return true;
}
return false; // Решта винятків будуть викинуті повторно
});
}
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ