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

Базова обробка через 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 чекає потік UIConfigureAwait(false) каже await: «не намагайся повертатись на той самий SynchronizationContext — продовжуй у ThreadPool-потоці». Це ламає дедлок і підвищує продуктивність у бібліотечному коді:
Правило використання ConfigureAwait(false):
| Де | Рекомендація |
|---|---|
| Бібліотечний код (NuGet-пакети, спільні сервіси) | Завжди ConfigureAwait(false) — бібліотека не знає, де її використають |
| Код застосунку (UI, контролери) | Без ConfigureAwait(false) — потрібно оновлювати UI або залишатись у контексті HTTP-запиту |
| ASP.NET Core | Можна не турбуватись — SynchronizationContext відсутній, ConfigureAwait(false) нічого не змінює |