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