JavaRush /Курси /C# SELF /Кілька винятків: Aggregate...

Кілька винятків: 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. Обробка масових помилок

У реальних застосунках часто потрібно обробляти багато помилок різними способами. Наприклад, ви надсилаєте електронні листи всім користувачам, і частина відправок падає:

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 при різних способах очікування

Як очікуємо завдання Який виняток спіймаємо
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; // Решта винятків будуть викинуті повторно
    });
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ