OOP Course
Сьогодні

Підрозділ 17.4

Обробка помилок в async-методах

17.4. Обробка помилок в async методах Асинхронний код вимагає особливої уваги до обробки помилок. Виняток, кинутий у асинхронному методі, не поширюється миттєво — він «консервується» у Task і чекає, поки хтось

17.4. Обробка помилок в async-методах

Асинхронний код вимагає особливої уваги до обробки помилок. Виняток, кинутий у асинхронному методі, не поширюється миттєво — він «консервується» у Task і чекає, поки хтось зробить await. Якщо await ніколи не відбудеться, виняток може бути втрачений назавжди або з'явитися в непередбаченому місці.

Поширення помилок в async-методахПоширення помилок в async-методах

Базова обробка через try/catch навколо await

Найпростіший і найправильніший підхід: обгорніть await-вираз у try/catch. Компілятор гарантує, що виняток із завдання буде «розгорнутий» і кинутий у точці await:

try/catch навколо await перехоплює виняток точно так само, як при синхронному виклику. Це одна з головних переваг async/await над старішими підходами (callback, ContinueWith) — обробка помилок виглядає природньо.

async void і загублені винятки

Найнебезпечніша помилка в асинхронному коді — виняток у методі async void. Оскільки async void не повертає Task, кидати й перехоплювати виняток нема де: він потрапляє безпосередньо у SynchronizationContext або у ThreadPool, що призводить до аварійного завершення застосунку:

Чому приклад не кидає виняток насправді? Тому що в консольному застосунку немає SynchronizationContext: виняток з async void повторно кидається на потоці пулу як необроблений, і процес аварійно завершується, — до наступного рядка програма просто не дійшла б. У WPF/WinForms такий виняток потрапляє в цикл повідомлень UI і завершує застосунок, якщо його не перехопить глобальний обробник.

Правило абсолютне: ніколи не використовуйте async void поза обробниками подій. Якщо вам потрібна операція «запустив і забув» без очікування — збережіть Task у змінну (і переконайтесь, що виняток десь оброблюється):

Обробка кількох помилок з Task.WhenAll

Коли Task.WhenAll чекає кількох завдань, і деякі з них завершуються з помилкою, await Task.WhenAll(...) розгортає лише першу помилку. Інші помилки залишаються у AggregateException, доступній через task.Exception:

Паттерн: зберігати посилання на Task<T[]>, що повертає WhenAll, і перевіряти task.Exception.InnerExceptions для повного списку помилок.

finally у async-методах

finally-блок у async-методах працює так само, як у синхронних: виконується незалежно від того, успішно чи з помилкою завершився try-блок. Це важливо для гарантованого звільнення ресурсів:

await можна використовувати і в catch, і в finally-блоках (починаючи з C# 6). Це дозволяє виконувати асинхронне очищення ресурсів:

Глобальна обробка необроблених виключень

У продакшн-застосунках важливо передбачити глобальний обробник для необроблених async-виключень. Для консольних застосунків та ASP.NET Core це AppDomain.CurrentDomain.UnhandledException та TaskScheduler.UnobservedTaskException:

UnobservedTaskException спрацьовує, коли Task з необробленим винятком збирається GC (тому момент спрацювання непередбачуваний і в прикладі його доводиться провокувати GC.Collect()). Починаючи з .NET Framework 4.5, «забутий» виняток задачі за замовчуванням не завершує процес — подія лише дає змогу його залогувати; SetObserved() позначає виняток як оброблений. Натомість AppDomain.CurrentDomain.UnhandledException дозволяє лише залогувати необроблений виняток потоку (зокрема з async void) — скасувати падіння процесу він не може. Це остання лінія захисту, але покладатись на неї як на основний механізм обробки помилок — погана практика.

Типові помилки та їх рішення

1. Забув await — виняток загублений:

2. .Result або .Wait() у async-контексті — дедлок:

Виклик .Result або .Wait() у async-контексті з SynchronizationContext (ASP.NET Classic, WinForms, WPF) призводить до дедлоку: await чекає потоку UI, а потік UI заблокований на .Result. Вирішення — завжди використовувати await:

Загальне правило: async вниз по стеку до кінця. Якщо один метод async — всі методи, що його викликають, теж мають бути async. Змішування синхронного і асинхронного коду через .Result є джерелом найважчих для діагностики дедлоків.

ConfigureAwait(false) та SynchronizationContext

Щоб зрозуміти ConfigureAwait(false), потрібно спочатку зрозуміти SynchronizationContext. Це механізм, що дозволяє продовженню (continuation) після await повернутися до «правильного» потоку:

  • У WinForms/WPF: після await виконання продовжується у потоці UI — щоб можна було оновити елементи форми
  • У ASP.NET Classic: після await виконання повертається до того самого HTTP-потоку
  • У ASP.NET Core і консольних застосунках: SynchronizationContext відсутній, await продовжується у довільному ThreadPool-потоці

Саме через SynchronizationContext і виникає класичний дедлок з .Result:

1. Потік UI (або HTTP-потік) викликає GetDataAsync().Result
2. GetDataAsync() всередині робить await Task.Delay(100)
3. Task.Delay завершується — continuation чекає захоплення потоку UI
4. Але потік UI заблокований на .Result!
5. Дедлок: потік UI чекає Task, Task чекає потік UI

ConfigureAwait(false) каже await: «не намагайся повертатись на той самий SynchronizationContext — продовжуй у ThreadPool-потоці». Це ламає дедлок і підвищує продуктивність у бібліотечному коді:

Правило використання ConfigureAwait(false):

Де Рекомендація
Бібліотечний код (NuGet-пакети, спільні сервіси) Завжди ConfigureAwait(false) — бібліотека не знає, де її використають
Код застосунку (UI, контролери) Без ConfigureAwait(false) — потрібно оновлювати UI або залишатись у контексті HTTP-запиту
ASP.NET Core Можна не турбуватись — SynchronizationContext відсутній, ConfigureAwait(false) нічого не змінює
Розроблено Tomka Yurii · © 2026 ·