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. Обработка массовых ошибок
В реальных приложениях часто нужно обрабатывать множество ошибок по-разному. Например, вы рассылаете e-mail всем пользователям, и часть отправок падает:
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.
Pattern: 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; // Остальные исключения будут выброшены заново
});
}
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ