«Хороший сайт делается для людей, а поисковики приходят следом»

— из принципов редакции SeoLiga.ru
Главная » Статьи » .NET и C# » Обработка исключений: правила хорошего тона

Обработка исключений: правила хорошего тона

Исключения — один из самых недооценённых механизмов платформы .NET. Новички либо оборачивают весь код в один огромный блок перехвата, либо вовсе не думают об ошибках, пока приложение не упадёт у заказчика. И то и другое приводит к одному результату: сложно искать причины, а пользователи видят невнятные сообщения.

В этой статье соберём правила, которые помогают писать надёжный код: когда выбрасывать исключение, как и где его перехватывать, что логировать, как освобождать ресурсы и чего категорически не делать.

Что такое исключение и когда оно уместно

Исключение сообщает, что операция не может быть выполнена так, как задумано. Не «пользователь ввёл неправильное значение», а «программа оказалась в ситуации, которую её код не ожидал или не может обработать на текущем уровне».

Хорошая проверка: если событие происходит регулярно и ожидаемо, это не исключительная ситуация. Конец файла при чтении, отсутствие записи в словаре, неверный формат строки от пользователя — всё это повседневные события. Для них лучше использовать возвращаемые значения: методы вида «попробовать разобрать», проверки наличия ключа, булевы флаги результата.

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

Правило первое: перехватывайте только то, что умеете обработать

Блок перехвата должен делать что-то осмысленное: восстановиться, повторить, подставить значение по умолчанию, преобразовать ошибку в более понятную или записать её и сообщить пользователю. Если ничего из этого вы сделать не можете, не перехватывайте. Пусть исключение поднимется выше, туда, где решение есть.

Самый вредный антипаттерн — пустой перехват:

try
{
    SaveReport(data);
}
catch (Exception)
{
}

Ошибка проглочена, программа продолжает работу как ни в чём не бывало, отчёт не сохранён, а причину никто не узнает. Если вы всё же уверены, что ошибку можно игнорировать, оставьте комментарий, почему, и хотя бы запишите факт в журнал.

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

Правило второе: сохраняйте стек вызовов

Когда вы перехватили исключение, но хотите передать его дальше, используйте повторный вызов без указания объекта:

catch (IOException ex)
{
    logger.Error(ex, "Не удалось прочитать файл настроек");
    throw;
}

Вариант с указанием объекта сбрасывает стек и теряет информацию о том, где ошибка произошла на самом деле. Разница критична при разборе инцидентов: вместо точного места вы увидите лишь строку, где исключение выбросили повторно.

Если нужно обернуть исключение в более высокоуровневое, передавайте исходное как внутреннее:

catch (SqlException ex)
{
    throw new OrderStorageException("Не удалось сохранить заказ", ex);
}

Так верхний слой получает понятное сообщение предметной области, а при необходимости разработчик спустится по цепочке до первопричины.

Правило третье: освобождайте ресурсы гарантированно

Файлы, соединения, потоки, сокеты нужно закрывать независимо от того, успешно ли завершилась операция. Для этого есть блок finally и, что удобнее, оператор using.

using (var reader = new StreamReader(path))
{
    var text = reader.ReadToEnd();
    Process(text);
}

Как только выполнение покидает блок, нормально или из-за ошибки, ресурс освобождается. Это короче и надёжнее, чем вручную писать закрытие в finally. Если ваш класс владеет неуправляемыми ресурсами, реализуйте интерфейс освобождения и пользуйтесь им так же.

Помните: в finally не должны выбрасываться новые исключения. Иначе исходная ошибка будет потеряна, а вы получите вторичную, куда менее информативную.

Правило четвёртое: проверяйте входные данные заранее

Публичные методы должны проверять аргументы в начале и сразу сообщать о нарушении контракта. Для этого есть стандартные типы: для пустой ссылки, для значения вне допустимого диапазона, для неверного состояния объекта.

  • Пустую ссылку там, где она недопустима, сообщайте исключением аргумента с указанием его имени.
  • Значение вне диапазона сопровождайте сообщением, что именно ожидалось.
  • Если метод нельзя вызывать в текущем состоянии объекта, используйте исключение недопустимой операции.

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

Правило пятое: создавайте собственные типы с умом

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

Рекомендации:

  • наследуйте от базового типа исключений, а не от устаревших специализированных веток;
  • давайте имени окончание, указывающее на исключение;
  • добавляйте конструкторы с сообщением и внутренним исключением;
  • включайте в тип полезные свойства: идентификатор заказа, имя параметра, код ошибки;
  • не создавайте иерархию из двадцати классов, если вызывающий код различает только два случая.
public class OrderNotFoundException : Exception
{
    public int OrderId { get; }

    public OrderNotFoundException(int orderId)
        : base($"Заказ {orderId} не найден")
    {
        OrderId = orderId;
    }
}

Правило шестое: логируйте один раз и в нужном месте

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

В запись помещайте:

  1. тип и сообщение исключения;
  2. полный стек вызовов;
  3. внутренние исключения;
  4. контекст: идентификатор пользователя или запроса, входные параметры операции;
  5. время и версию приложения.

Не пишите в журнал пароли, ключи и персональные данные, даже если они были в параметрах операции.

Единая точка обработки на верхнем уровне

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

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

Асинхронный код и исключения

В асинхронных методах исключение попадает внутрь задачи и выбрасывается при ожидании её результата. Поэтому всегда ожидайте задачи и не оставляйте их «висеть» без присмотра: потерянное исключение никто не увидит.

  • Не используйте асинхронные методы без возвращаемого значения за пределами обработчиков событий: ошибки из них трудно перехватить.
  • При ожидании нескольких задач помните, что наружу вылетит только первое исключение, а остальные придётся смотреть в самой задаче.
  • Для отмены операций используйте специальный механизм и соответствующее исключение, а не обычные ошибки.

Чек-лист правил хорошего тона

  • Не используйте исключения для обычной логики.
  • Не перехватывайте то, что не умеете обработать.
  • Никогда не оставляйте пустой блок перехвата.
  • Пробрасывайте повторно без потери стека.
  • Освобождайте ресурсы через using или finally.
  • Проверяйте аргументы на входе.
  • Заводите собственные типы только когда они нужны вызывающему коду.
  • Логируйте один раз, с контекстом.
  • Показывайте пользователю понятное сообщение, а подробности оставляйте в журнале.

Заключение

Грамотная обработка исключений — это не множество блоков перехвата, а продуманная стратегия. Низкие уровни сообщают о проблемах точно и вовремя, средние добавляют контекст или превращают ошибки в понятия предметной области, верхний собирает всё в единую точку, записывает и аккуратно информирует пользователя.

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

АВ
Андрей Ветров
Автор SeoLiga.ru. Пишет о практике: что работает на реальных проектах, а что лучше обойти стороной. Хотите предложить свою статью? Загляните в раздел «Авторам».