JavaRush /Курси /C# SELF /Зведена стратегія обробки помилок:

Зведена стратегія обробки помилок: Task, async/ await, AggregateException

C# SELF
Рівень 61 , Лекція 4
Відкрита

1. Вступ

Писати програми, які ніколи не падають, — це як вірити, що баґи бояться вашої IDE. Насправді, що складнішим стає код (особливо в багатопоточному та асинхронному середовищі), то винахідливішими стають баґи, а помилки — витонченішими. Ігноруючи винятки в потоках і задачах, легко дістати витоки ресурсів, зависання, втрату даних або неочікувані збої через години після запуску.

У цій лекції ми зберемо докупи найкращі практики обробки помилок у багатопоточному та асинхронному програмуванні на C#. Ви дізнаєтеся, як грамотно ловити винятки, як розбирати «купу» одночасних помилок (у стилі AggregateException), що робити із задачами, запущеними «в нікуди», і чому ігнорувати винятки — небезпечно й безглуздо.

Чому все не так просто

  • Потік, у якому виникає помилка, може відрізнятися від потоку, де розміщено try-catch.
  • Асинхронні задачі не викидають винятки одразу — їх «пакують», і вони чекають обробки під час await (або синхронного очікування).
  • За одночасної роботи з кількома задачами (наприклад, Task.WhenAll) може виникнути кілька помилок — це треба враховувати.
  • Операції типу fire-and-forget можуть повністю «загубити» виняток без явного обробника.

Ця особливість — розділення контексту виконання. Уявіть програму як цирк із кількома аренами: якщо на одній сталася пожежа, її не відразу видно на інших. Важливо вміти правильно відстежувати й гасити такі «пожежі».

2. Винятки в задачах: Task і Task<TResult>

Як задачі сигналізують про помилки

Коли в задачі трапляється необроблений виняток, він не «вилетить» одразу назовні. Задача переходить у стан Faulted, а виняток зберігається всередині. Отримати його можна:

  • очікуючи завершення за допомогою await (або через task.Wait()/task.Result — але так краще не робити);
  • перевіривши властивість task.Exception — там буде AggregateException.

Приклад


// Асинхронний метод із помилкою
async Task FailAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Щось пішло не так");
}

async Task MainAsync()
{
    try
    {
        await FailAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Помилка: {ex.Message}");
    }
}

Якщо не поставити try-catch, програма впаде. Якщо поставити — виняток коректно перехопиться, навіть якщо помилка виникла в іншій задачі.

AggregateException: помилки оптом

Під час await Task.WhenAll(tasks) помилки з кількох задач збираються в один AggregateException (у його InnerExceptions).


async Task MultiFailAsync()
{
    Task t1 = Task.Run(() => throw new InvalidOperationException("Помилка 1"));
    Task t2 = Task.Run(() => throw new ArgumentException("Помилка 2"));
    try
    {
        await Task.WhenAll(t1, t2);
    }
    catch (Exception ex)
    {
        if (ex is AggregateException agg)
        {
            foreach (var e in agg.InnerExceptions)
                Console.WriteLine($"Виняток: {e.Message}");
        }
        else
        {
            Console.WriteLine($"Одиночна помилка: {ex.Message}");
        }
    }
}

Увага: під час використання await Task.WhenAll(tasks) .NET «розгортає» AggregateException, і в catch ви отримаєте «перший» виняток. Повний список доступний через task.Exception.InnerExceptions, якщо задача завершилась із помилкою.

3. «Fire and Forget»: чому не можна просто забути про задачу

Підхід «запусти й забудь» часто веде до прихованих збоїв. Приклад:


Task.Run(() => { throw new Exception("Бах!"); }); // Помилка "відлітає" в порожнечу.

Сучасне середовище виконання зберігає помилку в задачі й може завершити процес через необроблений виняток. Найкраще — зберігати посилання на задачу та/або підписуватися на подію TaskScheduler.UnobservedTaskException.

Як правильно?

  • Зберігайте задачу, щоб дочекатися її завершення й обробити помилку;
  • для fire‑and‑forget поставте обробник прямо всередині делегата.

Task.Run(() => {
    try 
    {
        // Код, який може кинути виняток
    }
    catch (Exception ex)
    {
        // Логуємо, сповіщаємо, але не пропускаємо виняток назовні
        Console.WriteLine($"Помилка в fire-and-forget: {ex.Message}");
    }
});

4. Помилки в потоках (Thread): «ловити нічого?»

Виняток у новому Thread неможливо спіймати try-catch зовні — лише всередині тіла потоку.


var thread = new Thread(() =>
{
    try
    {
        throw new Exception("Помилка в потоці");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Спіймали помилку всередині потоку: {ex.Message}");
    }
});
thread.Start();

Якщо не поставити обробник, виняток завершить лише цей потік (якщо він фоновий: thread.IsBackground = true). Для нефонових потоків необроблений виняток може завершити весь процес. Завжди ставте try-catch усередині потоку.

Як повертати результат і помилки з потоку?

  • Використовуйте черги або колекції для передавання результатів і помилок;
  • подійну модель;
  • краще переходьте на задачі — з ними зручніше обробляти помилки.

5. Паралельні цикли: ловимо помилки по‑особливому

У паралельних циклах помилки з різних гілок агрегуються в AggregateException.


try
{
    Parallel.For(0, 5, i =>
    {
        if (i % 2 == 0)
            throw new Exception($"Помилка в ітерації {i}");
    });
}
catch (AggregateException ex)
{
    foreach (var e in ex.InnerExceptions)
        Console.WriteLine($"[Паралельний цикл] Помилка: {e.Message}");
}

Якщо треба логувати локальні збої й продовжувати інші гілки, ставте внутрішні try-catch у кожній гілці.

6. Обробка помилок під час скасування задач

Під час скасування через CancellationToken за домовленістю кидається OperationCanceledException — це не збій, а штатна зупинка.


async Task DoWorkAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100);
    }
}

// Десь у коді:
var cts = new CancellationTokenSource();
var task = DoWorkAsync(cts.Token);
cts.Cancel(); // Приблизно через 200 мс

try
{
    await task;
}
catch (OperationCanceledException)
{
    Console.WriteLine("Операцію скасовано!");
}

Окрім ThrowIfCancellationRequested(), багато методів, що підтримують токен (наприклад, Task.Delay, HttpClient.GetAsync), самі кинуть OperationCanceledException. Перевіряйте підтримку скасування.

7. Корисні нюанси

Підходи на всі випадки

  • Ставте try-catch на верхньому рівні асинхронних методів — так спіймаєте «втеклі» помилки.
  • Не ігноруйте задачі: зберігайте посилання, дочікуйтеся завершення, логуйте помилки.
  • Для паралельних операцій (Task.WhenAll / Parallel.ForEach) враховуйте AggregateException.
  • Відділяйте скасування від збоїв: ловіть OperationCanceledException окремо.
  • Логуйте помилки, особливо «тихі», що не завершують застосунок аварійно.
  • Зберігайте деталі: логуйте всі вкладені винятки, а не лише перший.
  • Обробляйте помилки «на місці»: якщо не знаєте, що робити, — хоча б залогуйте.

Де ловити помилку в багатопоточному та асинхронному коді

flowchart TD
    A[Головний потік / UI] -->|Запускає задачу| B[Task/async]
    B -->|Всередині задачі| C[try/catch всередині асинхронного методу]
    B -->|await у головному потоці| D[try/catch навколо await]
    A -->|Запускає Thread| E[Thread]
    E -->|Всередині| F[try/catch всередині потоку]
    B -->|Багато задач| G[Task.WhenAll / Parallel.ForEach]
    G -->|Помилка| H[AggregateException]

8. Типові помилки та «граблі»

Помилка № 1: очікування задачі через .Result або .Wait(). Можливе взаємоблокування та/або неочікуваний AggregateException.

Помилка № 2: запуск fire‑and‑forget без внутрішнього try-catch — задача може непомітно завершитися з помилкою, діагностики не буде.

Помилка № 3: пропущені помилки в паралельних циклах — частина роботи не виконана, а ви цього не помічаєте.

Помилка № 4: нерозрізнення скасування (OperationCanceledException) і реальних помилок.

Помилка № 5: логування лише першої помилки серед кількох задач — решта залишаються «в тіні».

Помилка № 6: повторне використання одного об’єкта Exception для всіх задач — у кожної помилки має бути свій екземпляр.

Помилка № 7: відсутність обробки винятків у UI‑потоці — фонову помилку не видно, інтерфейс поводиться непередбачувано.

1
Опитування
Vyniatky u klasychnykh Thread, рівень 61, лекція 4
Недоступний
Vyniatky u klasychnykh Thread
Obrobka pomylok v asynkhronnomu kodi
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ