Avatar ·

Зачем перехватывать и повторно выбрасывать исключение в C#?

Привет, коллеги! Я новичок в C# и часто вижу в чужом коде конструкцию try-catch, где внутри блока catch просто перебрасывают то же самое исключение с помощью `throw;`. Мне это кажется бессмысленным — зачем ловить ошибку, если ты её не обрабатываешь, а просто выбрасываешь снова? Почему бы не дать исключению всплыть самому? Например, я часто встречаю такой код: ```csharp public void DoSomething() { try { // какой-то опасный код int result = 100 / 0; } catch (Exception ex) { // ничего не делаем, просто перебрасываем throw; } } ``` На мой взгляд, это то же самое, что и просто не оборачивать код в try-catch. Ведь если убрать try-catch, исключение всё равно вылетит наружу. Но раз так пишут опытные разработчики, значит, в этом есть какой-то смысл? Может быть, я что-то упускаю? Объясните, пожалуйста, зачем это нужно и какие есть подводные камни. Также я заметил, что иногда пишут `throw ex;` вместо `throw;`. Это одно и то же или есть разница? Заранее спасибо за пояснения! <<>>

Зачем перехватывать и пробрасывать исключение?

Ваш вопрос абсолютно логичен для начинающего разработчика. Действительно, если вы просто ловите исключение и тут же выбрасываете его снова без каких-либо действий, это выглядит бессмысленно. Однако существует несколько весомых причин, когда такая конструкция оправдана и даже необходима.

Основные сценарии использования

  • Логирование ошибки — самая частая причина. Вы перехватываете исключение, записываете его детали в лог (файл, базу данных, систему мониторинга), а затем пробрасываете дальше, чтобы вышестоящий код мог корректно обработать ошибку.
  • Освобождение ресурсов или откат транзакции — если в блоке 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. Для освобождения ресурсов или трансформации исключений по-прежнему нужен классический подход. Используйте фильтры в основном для логирования или проверки условий без изменения потока исключения.