JavaRush /Курсы /C# SELF /Несколько исключений: Aggr...

Несколько исключений: AggregateException

C# SELF
61 уровень , 1 лекция
Открыта

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 при разных способах ожидания

Как ждем задачу Какое исключение поймаем
await Task.WhenAll()
Первая внутренняя ошибка
Task.WhenAll().Wait()
Всегда AggregateException
Task.WhenAll().Result
Всегда 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; // Остальные исключения будут выброшены заново
    });
}
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ