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) и обрабатывать исключения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ