Обработка ошибок в C#: try/catch и продвинутые техники

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в C#: от try/catch к профессиональным техникам

В мире промышленной разработки на C# и .NET обработка ошибок — это не просто конструкция try/catch. Это целая философия, определяющая надёжность, читаемость и отказоустойчивость вашего кода. На продвинутом уровне разработчик должен не просто ловить исключения, но и грамотно их проектировать, фильтровать, логировать и восстанавливать состояние приложения. В этой статье мы разберём ключевые аспекты обработки ошибок в C#, которые помогут вам писать более качественный и профессиональный код.

1. Глубже в try/catch/finally: управление ресурсами и контекстом

Базовая конструкция try-catch-finally знакома каждому, но давайте посмотрим на неё под другим углом. Главная цель блока finally — гарантированное освобождение ресурсов, даже если произошло исключение. В современных версиях C# для этого чаще используют using, но finally остаётся незаменимым при работе с неуправляемыми ресурсами или сложной логикой завершения.

public void ProcessFile(string path)
{
    FileStream? fileStream = null;
    try
    {
        fileStream = File.OpenRead(path);
        // Логика обработки файла
    }
    catch (FileNotFoundException ex)
    {
        // Специфичная обработка: файл не найден
        Log.Error($"Файл {path} не найден: {ex.Message}");
        throw; // Пробрасываем исключение дальше, сохраняя стек вызовов
    }
    catch (UnauthorizedAccessException)
    {
        Log.Warn($"Нет доступа к файлу: {path}");
        // Можно выполнить альтернативное действие, не пробрасывая исключение
        return;
    }
    finally
    {
        // Гарантированное освобождение ресурса
        fileStream?.Dispose();
    }
}

Обратите внимание на использование throw; (без аргумента) — это ключевой момент. В отличие от throw ex;, конструкция throw; сохраняет оригинальный стек вызовов, что критически важно для отладки. Никогда не используйте throw ex; в реальных проектах, так как это уничтожает информацию о месте возникновения исходной ошибки.

2. Пользовательские исключения: проектируем доменную модель ошибок

В больших приложениях стандартных исключений .NET часто недостаточно. Создание собственных классов исключений позволяет точно передавать контекст ошибки и упрощает обработку на верхних уровнях. Хорошее пользовательское исключение должно быть сериализуемым, содержать осмысленное сообщение и нести дополнительную информацию (например, идентификатор сущности).

[Serializable]
public class OrderProcessingException : Exception
{
    public int OrderId { get; }
    public string FaultyStep { get; }

    public OrderProcessingException(int orderId, string faultyStep, string message) 
        : base(message)
    {
        OrderId = orderId;
        FaultyStep = faultyStep;
    }

    public OrderProcessingException(int orderId, string faultyStep, string message, Exception inner) 
        : base(message, inner)
    {
        OrderId = orderId;
        FaultyStep = faultyStep;
    }

    // Конструктор для десериализации
    protected OrderProcessingException(
        System.Runtime.Serialization.SerializationInfo info,
        System.Runtime.Serialization.StreamingContext context) : base(info, context)
    {
        OrderId = info.GetInt32(nameof(OrderId));
        FaultyStep = info.GetString(nameof(FaultyStep)) ?? string.Empty;
    }

    public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
    {
        base.GetObjectData(info, context);
        info.AddValue(nameof(OrderId), OrderId);
        info.AddValue(nameof(FaultyStep), FaultyStep);
    }
}

// Использование
public void ValidateOrder(int orderId)
{
    try
    {
        // Логика валидации
    }
    catch (InvalidOperationException ex)
    {
        throw new OrderProcessingException(orderId, "Validation", "Ошибка валидации заказа", ex);
    }
}

Правило: наследуйтесь от Exception (или одного из его подклассов, например InvalidOperationException), делайте класс сериализуемым и всегда предоставляйте минимум три конструктора: без параметров, с сообщением, с сообщением и внутренним исключением.

3. Фильтры исключений (Exception Filters) и when

Фильтры исключений — мощная, но часто недооценённая возможность C# (доступна с C# 6). Ключевое слово when позволяет задать условие, при котором блок catch будет выполнен. Это избавляет от необходимости повторно пробрасывать исключения внутри catch и делает код чище.

public void ProcessPayment(decimal amount)
{
    try
    {
        // Вызов внешнего платёжного шлюза
        PaymentGateway.Charge(amount);
    }
    catch (PaymentException ex) when (ex.ErrorCode == "INSUFFICIENT_FUNDS")
    {
        // Логика для недостатка средств
        Log.Warn($"Недостаточно средств для оплаты {amount}");
        InitiateRefund();
    }
    catch (PaymentException ex) when (ex.ErrorCode == "TIMEOUT")
    {
        // Логика для таймаута — можно повторить попытку
        Log.Warn($"Таймаут при оплате {amount}, повторяем...");
        RetryPayment(amount);
    }
    catch (PaymentException ex) when (ex.ErrorCode.StartsWith("AUTH_"))
    {
        // Ошибки авторизации — логируем и пробрасываем дальше
        Log.Error($"Ошибка авторизации: {ex.ErrorCode}");
        throw; // Фильтр сработал, блок catch выполнен, но мы можем пробросить
    }
    // Общий catch для всех остальных PaymentException
    catch (PaymentException ex)
    {
        Log.Error($"Неизвестная ошибка оплаты: {ex.Message}");
        throw;
    }
}

Фильтры особенно полезны при логировании: вы можете поймать исключение, залогировать его и, если условие не выполнено, позволить исключению "пролететь" дальше без повторного проброса. Это снижает нагрузку на стек и упрощает отладку.

4. Best Practices и антипаттерны обработки ошибок

На продвинутом уровне важно не только знать, как ловить исключения, но и понимать, когда этого делать не стоит. Вот несколько ключевых принципов:

  • Не используйте исключения для управления потоком. Исключения — это дорогостоящая операция. Для ожидаемых сценариев (например, отсутствие элемента в коллекции) используйте возвращаемые значения или паттерн TryParse.
  • Ловите исключения на правильном уровне. Нижние слои (репозитории, сервисы) должны пробрасывать исключения вверх. Обрабатывайте их на границе модулей (контр

Блоги

Рекомендаций по книгам