1. Вступ
Почнемо з важливого питання: чому не можна просто довіритися асинхронним методам і сподіватися, що все завжди працюватиме безвідмовно? Операції з файлами нерідко спричиняють винятки — файл могли видалити, закінчилася памʼять, бракує прав або файл заблокований. У синхронному коді ви б перехоплювали їх у звичному try-catch-блоці. В асинхронному коді принцип той самий, але є нюанси: виняток може виникнути не одразу під час виклику методу, а пізніше, коли операція фактично виконується.
Момент виникнення помилки
У синхронному коді під час читання через StreamReader.Read() виняток виникне прямо в рядку виклику — перехопили в catch — і гаразд.
В асинхронному коді (await stream.ReadAsync()) виняток зʼявиться не в момент старту операції, а безпосередньо під час await — коли задача завершиться з помилкою. Якщо забути додати await, помилка може залишатися «невидимою» певний час.
2. Як ловити винятки в асинхронних методах
Давайте одразу погляньмо на типовий шаблон:
try
{
using FileStream fs = new FileStream("myfile.txt", FileMode.Open);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
// Подальша обробка...
}
catch (IOException ex)
{
Console.WriteLine("Помилка введення-виведення: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Немає доступу до файлу: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Невідома помилка: " + ex);
}
Так-так, усе просто — використовуємо знайомий try-catch, але в асинхронному методі. Важливо, щоб сам метод мав ключове слово async, інакше компілятор сваритиметься.
Важливий нюанс: де ставити await
Task<int> readTask = fs.ReadAsync(buffer, 0, buffer.Length);
// ... тут випадково забули await або обробку
У такому разі помилка, якщо вона станеться, потрапить у саму задачу (Task), і ви про неї не дізнаєтеся, доки не спробуєте отримати результат — наприклад, через await readTask або через властивість Task.Exception. Якщо взагалі забути про await, задача може завершитися з помилкою, а вам про це ніхто не повідомить.
3. Чому без обробки помилок асинхронний код підступний
Сценарій 1: «Fire and forget» — пастка новачка
FileStream fs = new FileStream("file.txt", FileMode.Open);
byte[] buffer = new byte[8000];
fs.ReadAsync(buffer, 0, buffer.Length);
// А далі програма живе собі своїм життям
Операція читання йде у «паралельне плавання», і якщо вона завершиться з помилкою, жоден catch її не перехопить. Виняток сховається всередині задачі. Цей патерн називають «fire and forget», і в реальних застосунках він загрожує пропуском критичних помилок і витоками ресурсів.
Сценарій 2: Асинхронні методи без await
Task t = MyAsyncMethod();
// ... тут щось робимо, а потім забули про t
Помилки, що сталися всередині MyAsyncMethod, не «піднімуться», доки ви явно не дочекаєтеся задачі (await t або t.Wait()).
4. Як правильно ловити помилки від асинхронних задач
Стратегія 1: Завжди використовуйте await
try
{
await SomeFileOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("Щось пішло не так: " + ex.Message);
}
Так виняток буде піднято саме в місці очікування і не загубиться.
Стратегія 2: Обробка за допомогою .ContinueWith
Якщо з якоїсь причини ви не використовуєте await, можна додати обробник помилок через ContinueWith:
var task = fs.ReadAsync(buffer, 0, buffer.Length);
task.ContinueWith(t =>
{
if (t.Exception != null)
Console.WriteLine("Помилка під час асинхронного читання: " + t.Exception.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
Відверто кажучи, у сучасних застосунках C# так роблять рідко — async/await робить код простішим і чистішим.
Можливі винятки під час роботи з файлами асинхронно
- IOException — збій диска, файл не знайдено, надто довгий шлях, пристрій недоступний.
- UnauthorizedAccessException — недостатньо прав доступу.
- ObjectDisposedException — потік закрито до завершення операції.
- OperationCanceledException — операцію скасовано через токен скасування (CancellationToken).
5. Приклад: Асинхронне читання з обробкою помилок
Додаймо цю логіку в наш застосунок:
using System;
using System.IO;
using System.Threading.Tasks;
namespace FileAsyncDemo
{
class Program
{
static async Task Main()
{
string path = "bigfile.txt";
byte[] buffer = new byte[4096];
try
{
using FileStream fs = new FileStream(path, FileMode.Open);
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine($"Прочитано {bytesRead} байт із {path}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Файл не знайдено: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Немає доступу до файлу: " + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("Помилка читання/запису: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Інша помилка: " + ex.Message);
}
}
}
}
Важливе зауваження
Якщо ви не використовуєте await у Main, а робите лише Task result = SomeAsyncMethod(); — помилки «мовчатимуть» і виявляться пізніше, коли ви таки спробуєте отримати результат.
6. Найчастіші помилки й «граблі» під час обробки помилок
Забувають ставити await на асинхронний метод — помилки не спливають вчасно, програма поводиться непередбачувано.
Не обгортають асинхронні виклики в try-catch — застосунок падає при першому ж збої.
Обробляють лише Exception, ігноруючи специфічні винятки на кшталт UnauthorizedAccessException або OperationCanceledException — у підсумку це ускладнює діагностику.
Використовують «fire and forget» задачі без явного логування помилок — винятки губляться всередині Task.
Вважають, що якщо задача завершилася — отже, усе успішно. Потрібно явно очікувати результат (await) й обробляти винятки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ