OOP Course
Сьогодні

Підрозділ 17.4

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

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

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

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

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

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

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

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

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

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

Правило абсолютне: ніколи не використовуйте 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. Це остання лінія захисту, але покладатись на неї як на основний механізм обробки помилок — погана практика.

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

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 ·