Поймать разом: как перехватить несколько исключений в C# без дублирования кода
Представьте: вы пишете приложение, которое работает с файлами, сетью и базами данных. В одном методе может вылететь FileNotFoundException, IOException или UnauthorizedAccessException. Классический подход — три блока catch с одинаковым кодом внутри. Это нарушает принцип DRY (Don't Repeat Yourself) и превращает код в "спагетти".
Хорошая новость: в C# есть несколько элегантных способов перехватить несколько исключений сразу. В этом туториале мы разберём все варианты — от базовых до продвинутых — и вы узнаете, как писать чистый, поддерживаемый код обработки ошибок.
Почему это важно? Реальный сценарий
Допустим, вы разрабатываете консольное приложение для резервного копирования. Оно читает файлы, сжимает их и отправляет на удалённый сервер. На каждом этапе могут возникнуть разные исключения:
- Чтение файла:
FileNotFoundException,PathTooLongException - Сжатие:
OutOfMemoryException - Отправка:
HttpRequestException,TimeoutException
Если вы будете писать отдельный catch для каждого исключения с одинаковым логированием, код станет громоздким. А если позже потребуется изменить логику — придётся править в трёх местах. Именно здесь и пригодится умение ловить несколько исключений за раз.
Способ 1. Классический: несколько блоков catch (до C# 6)
Самый простой и очевидный метод — написать несколько блоков catch подряд. C# сам выберет нужный блок по типу исключения:
try
{
// Код, который может выбросить разные исключения
string content = File.ReadAllText(@"C:\data\config.txt");
int value = int.Parse(content);
await SendToServerAsync(value);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"Файл не найден: {ex.Message}");
LogError(ex);
}
catch (FormatException ex)
{
Console.WriteLine($"Неверный формат данных: {ex.Message}");
LogError(ex);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Ошибка сети: {ex.Message}");
LogError(ex);
}
Плюсы: прозрачно, легко читается, можно обрабатывать каждое исключение по-своему.
Минусы: если действия одинаковые — дублирование кода.
Способ 2. Перехват нескольких исключений в одном catch (C# 6+)
Начиная с C# 6, вы можете перечислить несколько типов исключений через | в одном блоке catch. Это уменьшает дублирование и сохраняет читаемость:
try
{
// Рискованный код
}
catch (FileNotFoundException | FormatException | HttpRequestException ex)
{
Console.WriteLine($"Произошла ошибка: {ex.Message}");
LogError(ex);
// Дополнительная логика, общая для всех трёх типов
}
Важно: все исключения должны быть совместимы по иерархии. Нельзя смешивать, например, IOException и NullReferenceException — они не связаны общим предком, кроме Exception. Но в C# 6+ это разрешено, компилятор просто приведёт переменную ex к общему базовому типу.
Когда использовать этот способ?
- Когда логика обработки идентична для всех исключений.
- Когда вы хотите избежать вложенных блоков
ifилиswitch. - В небольших методах, где важна краткость.
Способ 3. Фильтры исключений (C# 6+)
Фильтры исключений — мощный механизм, который позволяет ловить исключения по условию с помощью ключевого слова when. Вы можете перехватить одно и то же исключение, но с разной логикой в зависимости от его свойств:
try
{
// Код
}
catch (IOException ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException)
{
Console.WriteLine("Проблема с файлом или папкой");
}
catch (IOException ex) when (ex is UnauthorizedAccessException)
{
Console.WriteLine("Нет прав доступа");
}
catch (Exception ex) when (DateTime.Now.DayOfWeek == DayOfWeek.Monday)
{
Console.WriteLine("По понедельникам ловим все ошибки!");
}
Ключевой момент: фильтры выполняются до входа в блок catch. Если условие when возвращает false, исключение не перехватывается, и стек поиска продолжается. Это позволяет создавать очень гибкие конструкции.
Реальный пример с фильтрами
Представьте, что вы обрабатываете загрузку файлов. Ошибка "файл занят" (статус блокировки) должна прервать операцию, а "файл не найден" — просто записаться в лог:
try
{
UploadFile("report.xlsx");
}
catch (IOException ex) when (ex.Message.Contains("занят другим процессом"))
{
Console.WriteLine("Файл заблокирован. Операция отменена.");
throw; // Пробрасываем дальше
}
catch (IOException ex) when (ex.Message.Contains("не найден"))
{
LogWarning(ex);
// Продолжаем выполнение
}
Фильтры исключений — это элегантный способ избежать вложенных if внутри блока catch.
Способ 4. Общий предок: базовый класс Exception
Иногда самый простой способ — перехватить все исключения через базовый класс Exception и уже внутри проверить тип:
try
{
// Код
}
catch (Exception ex) when (ex is IOException or HttpRequestException or FormatException)
{
Console.WriteLine($"Ошибка: {ex.Message}");
// Общая обработка
}
Или без фильтра, но с проверкой внутри:
try
{
// Код
}
catch (Exception ex)
{
if (ex is IOException || ex is HttpRequestException || ex is FormatException)
{
Console.WriteLine($"Ошибка: {ex.Message}");
}
else
{
throw; // Пробрасываем неожиданные исключения
}
}
Предупреждение: перехватывать Exception без фильтра — плохая практика. Вы рискуете проглотить критические ошибки вроде OutOfMemoryException или StackOverflowException. Всегда используйте when или пробрасывайте необработанные исключения.
Способ 5. AggregateException и Task.WhenAll
Если вы работаете с асинхронными операциями, особенно с Task.WhenAll, исключения могут быть агрегированы в AggregateException. Перехватить все внутренние исключения можно так:
try
{
Task task1 = DownloadFileAsync("file1.zip");
Task task2 = DownloadFileAsync("file2.zip");
await Task.WhenAll(task1,