Menu

try catch в C#: обработка исключений через catch, when и finally

try, catch и finally это способ обработки исключений в C#. Как исключение поднимается по стеку вызовов, как перехватывать конкретные типы исключений в правильном порядке, фильтровать через when, освобождать ресурсы в finally, повторно выбрасывать через throw; без потери трассировки стека и какие исключения не стоит перехватывать.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Когда во время выполнения что-то идёт не так (файла нет, текст не является числом, ключа нет в словаре), .NET выбрасывает исключение: объект, описывающий ошибку. Исключение поднимается по стеку вызовов, пока его не обработает блок catch. Если этого не делает никто, программа останавливается и печатает ошибку.

Простой try catch

Оберните код, который может завершиться ошибкой, в try, а ошибку обработайте в catch:

Вывод:

42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running

Когда int.Parse("forty-two") выбрасывает исключение, Console.WriteLine после него в блоке try пропускается, выполняется блок catch (FormatException), и цикл продолжается. Переменную можно опустить (catch (FormatException)), когда объект исключения не нужен.

Для этого конкретного случая есть инструмент лучше: int.TryParse(input, out int n) возвращает false вместо исключения. Исключения предназначены для ситуаций, которых код не ожидает; ввод, который часто бывает некорректным, ожидаем, поэтому проверяйте его, а не перехватывайте.

Как движется исключение

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

Вывод:

OrderTotal finished
5.00
Caught KeyNotFoundException in Main

У PriceOf и OrderTotal нет catch, поэтому исключение проходит сквозь них прямо в Main. Ставьте обработчик на том уровне, который знает, что делать с ошибкой, и часто это не то место, где она произошла.

Перехват конкретных исключений в правильном порядке

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

Вывод:

10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException

Последний вызов выбрасывает OverflowException, который не обрабатывает ни одно конкретное предложение, поэтому его обрабатывает общий catch (Exception e). Если поставить catch (Exception) первым, предложения после него станут недостижимыми, и компилятор отвергнет это ошибкой CS0160.

Фильтры исключений через when

C# 6 добавил when: условие, которое решает, применяется ли предложение catch. Если оно равно false, среда выполнения продолжает искать другой обработчик, как будто этого предложения нет.

Вывод:

Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401

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

finally: код, который выполняется всегда

Блок finally выполняется, когда управление покидает try, будь то нормальное завершение, досрочный возврат или исключение. Здесь размещается освобождение ресурсов. (Одна оговорка: если исключение нигде не перехвачено, процесс может завершиться, не выполнив его.)

Вывод:

Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error

В каждом случае «Close connection» печатается до того, как результат метода дойдёт до Main: finally выполняется после вычисления значения return, но до фактического возврата из метода. У try может быть finally вообще без catch: тогда ресурсы освобождаются, а исключение продолжает путь к вызывающему коду.

Для объектов, реализующих IDisposable (файлы, потоки, подключения), такой try/finally за вас пишет оператор using.

Повторный выброс: throw; или throw e;

Иногда блок catch что-то записывает в лог и затем позволяет исключению продолжить путь. От способа повторного выброса зависит, сохранится ли трассировка стека:

Вывод:

throw;   trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False

throw e; считает исключение заново выброшенным из блока catch, поэтому кадры ниже него, включая метод, где произошла ошибка, пропадают из трассировки. Всегда выбрасывайте повторно голым throw;. (Атрибут NoInlining нужен только потому, что JIT может встроить такой маленький метод в вызывающий, и тогда он исчез бы из обеих трассировок.)

Чтобы вместо этого добавить контекст, оберните исключение в новое и передайте исходное как внутреннее: throw new ConfigException("Could not start the app", e);. Внутреннее исключение сохраняет собственную трассировку стека, а логгеры печатают всю цепочку. Как писать собственные типы исключений, описано на странице выброс исключений.

Объект Exception

Каждое исключение наследуется от System.Exception. Чаще всего используются такие члены:

ЧленЧто содержит
MessageОписание, понятное человеку
GetType().NameТип исключения, например FormatException
StackTraceЦепочку вызовов методов в точке выброса
InnerExceptionИсключение, вызвавшее это, или null
ToString()Тип, сообщение, внутренние исключения и трассировку стека вместе

Пишите в лог e.ToString(), а не e.Message, когда хотите потом разобраться в сбое: одно сообщение редко говорит, где была проблема. Не показывайте e.ToString() конечным пользователям.

Частые типы исключений

ИсключениеТипичная причина
NullReferenceExceptionВызов члена у ссылки null
ArgumentNullExceptionМетоду передали null там, где нужно значение
ArgumentOutOfRangeExceptionАргумент или индекс списка вне допустимого диапазона
IndexOutOfRangeExceptionИндекс массива за его границами
FormatExceptionint.Parse, DateTime.Parse и подобные для текста в неверном формате
InvalidCastExceptionЯвное приведение к типу, которым объект не является
InvalidOperationExceptionОбъект в неподходящем для вызова состоянии (пустая последовательность, изменённая коллекция)
KeyNotFoundExceptionЧтение отсутствующего ключа словаря через индексатор
DivideByZeroExceptionДеление на ноль для целых чисел или decimal
OverflowExceptionРазобранное число, проверяемое преобразование или результат проверяемой арифметики не помещается в тип
FileNotFoundException, IOExceptionПроблемы с файловой системой

NullReferenceException, IndexOutOfRangeException и InvalidCastException почти всегда означают ошибку в коде. Исправляйте код, а не перехватывайте их.

Не глотайте исключения

Пустой catch скрывает все ошибки, включая те, которых вы не предвидели:

try
{
    SaveOrder(order);
}
catch (Exception)
{
    // nothing: the order silently was not saved
}

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

  • Перехватывайте только то, что можете обработать, на том уровне, который может это обработать.
  • Если перехватываете ради лога, выбрасывайте повторно через throw;, если программа действительно не может продолжить.
  • Перехватывайте Exception только на внешней границе: в Main, обработчике запроса, рабочем цикле.
  • Для ожидаемых случаев предпочитайте TryParse, TryGetValue и проверки на null вместо исключений. Выброс исключения медленнее проверки, а блок try, который ничего не выбрасывает, почти ничего не стоит.

Частые ошибки

  • catch (Exception) первым. Следующие предложения становятся недостижимыми (CS0160).
  • throw e; для повторного выброса. Он стирает исходную трассировку стека; используйте throw;.
  • Пустые блоки catch. Ошибки исчезают; как минимум запишите в лог и выбросьте повторно.
  • Исключения для управления потоком выполнения. Проверяйте ввод через TryParse, а не перехватывайте FormatException.
  • Показ пользователям e.Message исключения фреймворка. Формулировки различаются между версиями .NET и написаны для разработчиков.

Часто задаваемые вопросы

Как работает try catch в C#?

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

Всегда ли выполняется finally в C#?

Почти всегда: после нормального завершения блока try, после обработки исключения в catch, после return или break внутри блока и когда исключение проходит насквозь к catch выше по стеку вызовов. Он не выполняется, когда процесс завершается раньше: процесс убит, вызван Environment.FailFast, произошёл StackOverflowException, а в .NET Core и новее при исключении, которое никто не перехватывает и которое завершает процесс до выполнения блоков finally.

Как перехватить несколько исключений в C#?

Напишите несколько предложений catch, начиная с самого конкретного типа: catch (FileNotFoundException) перед catch (IOException) перед catch (Exception). Компилятор отвергает предложение, которое никогда не может быть достигнуто, потому что предыдущее уже перехватывает его тип. Чтобы обработать два несвязанных типа одинаково, используйте фильтр: catch (Exception e) when (e is FormatException || e is OverflowException).

Чем throw отличается от throw ex в C#?

Внутри catch оператор throw; повторно выбрасывает текущее исключение с его исходной трассировкой стека. throw ex; выбрасывает тот же объект, но сбрасывает трассировку стека на текущую строку, поэтому метод, где на самом деле произошла ошибка, исчезает из трассировки. Используйте throw; или оберните исключение: throw new MyException("context", ex);.

Что такое фильтр исключений в C#?

Предложение when после catch (C# 6 и новее): catch (HttpRequestException e) when (e.Message.Contains("404")). Блок catch выполняется, только если условие равно true; иначе исключение продолжает искать другой обработчик, как будто этого предложения нет, и его стек не раскручивается.

Стоит ли перехватывать Exception в C#?

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

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ