Перейти к основному содержимому

Обработка исключений в C#

Разработчику Архитектору
Сначала — общая теория

Ошибка vs исключение в энциклопедии; в .NET исключения — управляемые отклонения, системные OutOfMemory — отдельный класс критичных сбоев.
Иерархия типов — Иерархия классов исключений в C#.


Обработка исключений в C#

Исключения

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

Ошибка (error) — это, как правило, системный сбой, который невозможно обработать (например, нехватка памяти). Исключение (exception) — это управляемое отклонение, которое можно предвидеть, перехватить и обработать.

Пример: Пользователь ввёл текст вместо числа. Это исключительная ситуация, которую программа может и должна обработать — например, попросить ввести число ещё раз. Таким образом, исключения — это часть логики приложения.


Интерактивное демо — часть сценариев на Python (try / except); в C# синтаксис свой, но стек вызовов и раскрутка те же. Подробнее: ошибки и исключения.

Play ITЗагрузка интерактивного демо…

Теория — обработка исключений

В .NET исключение — объект System.Exception, поднимающийся по стеку до catch; finally и using гарантируют освобождение ресурсов (аналог блока с гарантированным завершением). CLR может собрать отчёт об ошибке при необработанном сбое. Различайте исключение (перехватываемый сбой в логике или I/O) и фатальные состояния среды, близкие к Error в Java.

Общая теория — ошибки и исключения; дерево типов — Иерархия классов исключений в C#.


System.Exception

Все исключения в C# основаны на классе System.Exception. Это — базовый класс для всех исключений. .NET framework предоставляет класс System.Exception для обработки различных типов исключений, которые имеют место. Класс исключений является базовым классом среди других классов исключений.

System.Exception
├── SystemException (базовый класс для исключений среды .NET)
├── ArgumentException
│ ├── ArgumentNullException
│ └── ArgumentOutOfRangeException
├── InvalidOperationException
├── NullReferenceException
├── IOException
├── FormatException
└── DivideByZeroException

Разбор:

  • Диаграмма показывает дерево наследования исключений в .NET.
  • System.Exception — корневой базовый класс для всех исключений.
  • Более специфичные типы (ArgumentNullException, FormatException) помогают точнее диагностировать проблему.
  • В обработке лучше сначала ловить узкие типы, а Exception оставлять последним.

Все исключения — это объекты. Это значит, что у них есть свойства, методы и можно их расширять.


try, catch, finally

Основные блоки – try, catch, finally.

try {
// Блок кода, в котором может произойти ошибка
}
catch (ExceptionType ex) {
// Обработка исключения
}
finally {
// Код, который выполнится всегда (например, освобождение ресурсов)
}

Разбор:

  • try оборачивает код, где может возникнуть ошибка.
  • catch перехватывает исключение указанного типа и даёт доступ к объекту ошибки (ex).
  • finally выполняется независимо от того, была ошибка или нет.
  • Такая конструкция отделяет "обычную" логику от аварийной.

try — блок с "опасным" кодом. Содержит код, который может выбросить исключение.

try
{
int number = int.Parse(input); // Может выбросить FormatException
Console.WriteLine(100 / number); // Может выбросить DivideByZeroException
}

Разбор:

  • int.Parse(input) может завершиться FormatException, если строка не число.
  • Деление 100 / number может выбросить DivideByZeroException, если number == 0.
  • Оба потенциально опасных шага сгруппированы в одном try.
  • Это упрощает централизованную обработку ошибок ниже в catch.

catch — обработка исключения. Выполняется, если в try возникло исключение указанного типа.

catch (FormatException ex)
{
Console.WriteLine("Неверный формат числа.");
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Деление на ноль!");
}

Разбор:

  • Первый catch обрабатывает ошибки формата входных данных.
  • Второй catch отдельно ловит деление на ноль.
  • Разделение по типам позволяет выдавать точные сообщения пользователю.
  • Порядок catch важен: более конкретные типы должны идти раньше общих.

Можно ловить общее исключение:

catch (Exception ex)
{
Console.WriteLine($"Произошла ошибка: {ex.Message}");
}

Разбор:

  • catch (Exception ex) перехватывает почти любые необработанные ошибки.
  • ex.Message содержит текстовое описание причины исключения.
  • Интерполяция строки $"..." подставляет сообщение в вывод.
  • Такой блок полезен как fallback, но лучше дополнять его специализированными catch.

Ловить Exception — последнее средство. Лучше указывать конкретные типы.

Можно использовать несколько catch-блоков, чтобы обработать разные типы исключений по-разному:

try {
// ...
} catch (FileNotFoundException ex) {
Console.WriteLine("Файл не найден: " + ex.Message);
} catch (UnauthorizedAccessException ex) {
Console.WriteLine("Нет доступа: " + ex.Message);
} catch (Exception ex) {
Console.WriteLine("Другая ошибка: " + ex.Message);
}

Разбор:

  • Пример показывает каскад обработчиков от узких к широким типам.
  • FileNotFoundException и UnauthorizedAccessException обрабатываются отдельно.
  • Последний catch (Exception) ловит всё, что не подошло под предыдущие ветки.
  • Такой подход делает обработку ошибок предсказуемой и прозрачной.

При наличии нескольких catch-блоков важен порядок. Более специфичные исключения должны идти раньше, чем общие.

finally — блок, который выполняется всегда. Выполняется в любом случае — было ли исключение, был ли return, break или goto.

finally
{
// Освобождение ресурсов
file?.Close();
connection?.Dispose();
}

Разбор:

  • finally используется для обязательной очистки ресурсов.
  • file?.Close() вызывает метод только если file не null.
  • connection?.Dispose() корректно освобождает соединение.
  • Это защищает от утечек даже при исключениях и раннем выходе.

Он используется для закрытия файлов, освобождения соединений, очистки ресурсов. Начиная с C# 8, для таких задач лучше использовать using, но finally всё ещё актуален.

Когда finally может не выполниться? На практике — при аварийном завершении процесса: Environment.FailFast, необработанное исключение в финализаторе, принудительное убийство процесса ОС (kill -9), сбой питания. В нормальном управляемом коде при выходе из try/catch блок finally выполняется даже при return или повторном throw.

throw — генерация исключения вручную


if (age < 0)
{
throw new ArgumentException("Возраст не может быть отрицательным.", nameof(age));
}

Разбор:

  • Это guard-проверка входных данных перед основной логикой.
  • При age < 0 вручную выбрасывается ArgumentException.
  • nameof(age) безопасно передаёт имя параметра без "магической строки".
  • Такой стиль делает причину ошибки явной и облегчает диагностику.

Можно выбрасывать:

  • Новые исключения: throw new ArgumentException(...);
  • Перебрасывать текущее: throw; (сохраняет стек-трейс);
  • Перебрасывать с изменением: throw ex; (теряет оригинальный стек — плохо!).

Типы исключений

Типы исключений:

ИсключениеКогда возникает?
ArgumentExceptionПередан недопустимый аргумент
ArgumentNullExceptionАргумент равен null
ArgumentOutOfRangeExceptionАргумент выходит за пределы допустимого диапазона
InvalidOperationExceptionНедопустимая операция в текущем состоянии объекта
IOExceptionОшибки ввода/вывода (файлы, сеть)
NullReferenceExceptionПопытка вызвать метод у null-объекта
IndexOutOfRangeExceptionВыход за границы массива
FormatExceptionНеверный формат данных
DivideByZeroExceptionДеление на ноль (для целых чисел)

Совет: не ловите NullReferenceException — лучше проверяйте на null заранее.


Исключения, которые нельзя "остановить" в catch

Некоторые ситуации не предназначены для обычной обработки в catch:

СитуацияПоведение
StackOverflowExceptionПереполнение стека — процесс обычно завершается
OutOfMemoryException при исчерпании кучиМожет быть неперехватываемым в критическом состоянии
AccessViolationException / повреждение нативной памятиЧасто фатально для процесса
ThreadAbortExceptionУстаревший сценарий (.NET Framework); в .NET Core+ — не используйте Thread.Abort

В ASP.NET Core middleware перехватывает необработанные исключения запроса и возвращает ответ клиенту, но не спасает процесс от фатальных сбоев CLR.


Как читать стек-трейс (stack trace)?

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

Пример:

System.FormatException: Input string was not in a correct format.
at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)
at System.Number.ParseInt32(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)
at System.Int32.Parse(String s)
at MyApp.Program.ParseAge(String input) in Program.cs:line 15
at MyApp.Program.Main(String[] args) in Program.cs:line 8

Разбор:

  • Первая строка сообщает тип исключения и его текст.
  • Строки at ... показывают стек вызовов, по которому ошибка "поднималась" вверх.
  • Program.cs:line 15 указывает точную строку в пользовательском коде.
  • Анализируют обычно от пользовательских фреймов к системным, чтобы быстрее найти источник проблемы.

Как читать:

Первая строка — тип исключения и сообщение.

Строки ниже — порядок вызовов "снизу вверх":

  • Main вызвал ParseAge
  • ParseAge вызвал int.Parse
  • int.Parse выбросил исключение.

:line XX — номер строки в файле.

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


AggregateException

AggregateException — исключения в многопоточности. В асинхронном и параллельном программировании (например, Task.WhenAll, Parallel.ForEach) может возникнуть несколько исключений одновременно. .NET объединяет их в один объект — AggregateException.

Пример:

try
{
await Task.WhenAll(task1, task2, task3);
}
catch (AggregateException ex)
{
foreach (var inner in ex.InnerExceptions)
{
Console.WriteLine($"Ошибка: {inner.Message}");
}
}

Разбор:

  • Task.WhenAll(...) ждёт завершения всех задач одновременно.
  • При нескольких сбоях ошибки собираются в AggregateException.
  • ex.InnerExceptions содержит список внутренних исключений.
  • foreach позволяет вывести или обработать каждую ошибку отдельно.

AggregateException содержит коллекцию InnerExceptions, каждая из которых — отдельная ошибка.

Можно создавать свои классы исключений. Главное чтобы они наследовались от исключений.

public class InsufficientFundsException : Exception
{
public decimal Balance { get; }
public decimal Amount { get; }

public InsufficientFundsException(decimal balance, decimal amount)
: base($"Недостаточно средств: баланс {balance}, запрошено {amount}.")
{
Balance = balance;
Amount = amount;
}
}

Разбор:

  • Создаётся пользовательское доменное исключение, унаследованное от Exception.
  • Свойства Balance и Amount сохраняют полезный контекст ошибки.
  • Конструктор формирует понятное сообщение через base(...).
  • Такой тип удобно ловить отдельно и использовать в бизнес-правилах финансовых операций.