Зачем перехватывать и повторно выбрасывать исключение в C#?
Зачем перехватывать и пробрасывать исключение?
Ваш вопрос абсолютно логичен для начинающего разработчика. Действительно, если вы просто ловите исключение и тут же выбрасываете его снова без каких-либо действий, это выглядит бессмысленно. Однако существует несколько весомых причин, когда такая конструкция оправдана и даже необходима.
Основные сценарии использования
- Логирование ошибки — самая частая причина. Вы перехватываете исключение, записываете его детали в лог (файл, базу данных, систему мониторинга), а затем пробрасываете дальше, чтобы вышестоящий код мог корректно обработать ошибку.
- Освобождение ресурсов или откат транзакции — если в блоке try вы начали операцию, которая требует очистки при ошибке (например, открыли файл, начали транзакцию БД), вы можете перехватить исключение, выполнить очистку и снова выбросить исключение.
- Добавление контекста — вы можете перехватить низкоуровневое исключение и выбросить новое, более высокоуровневое, сохранив оригинальное как внутреннее (inner exception).
Правильный пример с логированием
public void ProcessData(string filePath)
{
try
{
var data = File.ReadAllText(filePath);
// ... обработка данных
}
catch (IOException ex)
{
// Логируем ошибку с дополнительной информацией
Logger.LogError($"Ошибка при чтении файла {filePath}: {ex.Message}");
// Пробрасываем то же самое исключение
throw;
}
}
Пример с освобождением ресурсов
public void TransferMoney(Account from, Account to, decimal amount)
{
// Начинаем транзакцию
using var transaction = database.BeginTransaction();
try
{
from.Withdraw(amount);
to.Deposit(amount);
transaction.Commit();
}
catch
{
// Откатываем транзакцию при любой ошибке
transaction.Rollback();
// Пробрасываем исключение дальше
throw;
}
}
Критическое отличие: throw vs throw ex
Это очень важный момент, который часто путают новички:
throw;— сохраняет полный стек вызовов (stack trace) оригинального исключения. Это правильный способ проброса.throw ex;— сбрасывает стек вызовов, начиная с текущего метода. Вы теряете информацию о том, где именно произошла оригинальная ошибка.
// ПЛОХОЙ вариант — теряем стек вызовов
catch (Exception ex)
{
// Стек начнется с этой строки, оригинальное место ошибки потеряно
throw ex;
}
// ХОРОШИЙ вариант — сохраняем стек
catch (Exception ex)
{
throw;
}
Когда НЕ нужно перехватывать и пробрасывать
Если вы не делаете ничего полезного (логирование, очистка, добавление контекста), то блок try-catch с одним throw — это мёртвый код. Просто уберите его. Исключение и так вылетит наружу без вашей помощи.
Добавление контекста с помощью нового исключения
catch (SqlException ex)
{
// Создаем новое исключение с понятным сообщением,
// но сохраняем оригинальное как innerException
throw new DataAccessException("Ошибка при обращении к базе данных", ex);
}
В этом случае вышестоящий код получит новое исключение, но сможет изучить детали через свойство InnerException.
Таким образом, конструкция "catch and rethrow" — это мощный инструмент, когда используется осмысленно. Главное правило: ловите исключение только если вы можете с ним что-то сделать (залогировать, освободить ресурс, трансформировать). В противном случае — не мешайте ему всплывать естественным путём.
<<Альтернативный взгляд: фильтры исключений
В C# 6.0 появилась более элегантная альтернатива для некоторых случаев — фильтры исключений (when). Они позволяют перехватывать исключение только при выполнении определённого условия, не теряя при этом оригинальный стек вызовов.
// Старый подход с логированием и пробросом
try
{
// код
}
catch (Exception ex)
{
Log(ex);
throw;
}
// Новый подход с фильтром — чище и производительнее
try
{
// код
}
catch (Exception ex) when (Log(ex))
{
// Этот блок никогда не выполнится,
// но исключение будет залогировано
}
// Вспомогательный метод
static bool Log(Exception ex)
{
Console.WriteLine($"Ошибка: {ex.Message}");
return false; // всегда false, чтобы не перехватывать исключение
}
Преимущества фильтров:
- Оригинальный стек вызовов сохраняется без искажений.
- Код становится более декларативным.
- Отладчик не останавливается на catch-блоке (если условие ложно), что упрощает отладку.
Однако фильтры не заменяют полностью конструкцию catch-and-rethrow. Для освобождения ресурсов или трансформации исключений по-прежнему нужен классический подход. Используйте фильтры в основном для логирования или проверки условий без изменения потока исключения.