.NET Tutorial
Junior6 мин чтенияВопрос 9 из 17

Исключения в C#: try/catch/finally, throw vs throw ex, свои исключения?

Как работает выброс исключения

База данныхRepository.SaveOrderService.PlaceMainнет catch — выходиз методанет подходящегоcatch — выход,finallyвыполняетсяPlace(order)Save(order)INSERTошибка соединенияthrow DbExceptionисключение доходит до catch в Maincatch (DbException)— логирование,сообщениепользователю
База данныхRepository.SaveOrderService.PlaceMainнет catch — выходиз методанет подходящегоcatch — выход,finallyвыполняетсяPlace(order)Save(order)INSERTошибка соединенияthrow DbExceptionисключение доходит до catch в Maincatch (DbException)— логирование,сообщениепользователю
Раскрутка стека до обработчика

try / catch / finally

C#
FileStream? file = null;
try
{
    file = File.OpenRead(path);
    var data = Parse(file);
    Console.WriteLine($"Прочитано {data.Count} записей");
}
catch (FileNotFoundException ex)              // частные — первыми
{
    Console.WriteLine($"Файл не найден: {ex.FileName}");
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("Нет доступа к файлу");
}
catch (Exception ex)                          // общий — последним (или не ловить вовсе)
{
    logger.LogError(ex, "Неожиданная ошибка при чтении {Path}", path);
    throw;                                    // не можем обработать — пробрасываем дальше
}
finally
{
    file?.Dispose();                          // выполнится при любом исходе
}

Тот же код с using — ресурс освободится автоматически:

C#
using var file = File.OpenRead(path);        // Dispose в конце области видимости, даже при исключении
var data = Parse(file);

throw против throw ex

C#
try { ProcessOrder(order); }
catch (Exception ex)
{
    logger.LogError(ex, "Ошибка");
    throw ex;      // ПЛОХО: стек вызовов начнётся с этой строки
}

catch (Exception ex)
{
    logger.LogError(ex, "Ошибка");
    throw;         // ХОРОШО: исходный стек сохранён
}

catch (SqlException ex)
{
    throw new OrderSaveException($"Не удалось сохранить заказ {order.Id}", ex);   // ХОРОШО: обёртка с InnerException
}

throw — стек сохранён

at Repository.Save() line 42

at OrderService.Place() line 18

at Controller.Post() line 10

throw ex — стек обрезан

at Controller.Post() line 12
(место throw ex)

где на самом деле ошибка — неизвестно

throw — стек сохранён

at Repository.Save() line 42

at OrderService.Place() line 18

at Controller.Post() line 10

throw ex — стек обрезан

at Controller.Post() line 12
(место throw ex)

где на самом деле ошибка — неизвестно

Что теряется при throw ex

Иерархия исключений

Exception

SystemException

ApplicationException
(не используйте как базу)

ArgumentException
→ ArgumentNullException
→ ArgumentOutOfRangeException

InvalidOperationException
→ ObjectDisposedException

NullReferenceException

IOException
→ FileNotFoundException

OperationCanceledException
→ TaskCanceledException

FormatException,
KeyNotFoundException,
NotSupportedException

Exception

SystemException

ApplicationException
(не используйте как базу)

ArgumentException
→ ArgumentNullException
→ ArgumentOutOfRangeException

InvalidOperationException
→ ObjectDisposedException

NullReferenceException

IOException
→ FileNotFoundException

OperationCanceledException
→ TaskCanceledException

FormatException,
KeyNotFoundException,
NotSupportedException

Основные типы исключений .NET
Исключение Когда бросать / когда возникает
ArgumentNullException Обязательный аргумент равен null (ArgumentNullException.ThrowIfNull(x))
ArgumentOutOfRangeException Аргумент вне допустимого диапазона
ArgumentException Аргумент некорректен по другой причине
InvalidOperationException Операция недопустима в текущем состоянии объекта (заказ уже оплачен)
NotSupportedException Операция не поддерживается (запись в read-only коллекцию)
KeyNotFoundException Ключа нет в словаре (dict[key])
NullReferenceException Обращение к члену null — ошибка программиста, не бросайте вручную
OperationCanceledException Операция отменена через CancellationToken

Свои исключения

C#
public sealed class InsufficientFundsException : Exception
{
    public decimal Requested { get; }
    public decimal Available { get; }

    public InsufficientFundsException(decimal requested, decimal available)
        : base($"Недостаточно средств: запрошено {requested}, доступно {available}")
    {
        Requested = requested;
        Available = available;
    }
}

Правила: имя оканчивается на Exception, наследуется от Exception (или подходящего стандартного), передаёт сообщение и при необходимости innerException, содержит полезные данные для обработки. Не создавайте свой тип, если подходит стандартный.

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

C#
try { await http.GetAsync(url, ct); }
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    return null;                         // 404 — ожидаемо, возвращаем null
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
    throw;                               // отмена пользователем — пробрасываем
}

Фильтр проверяется до раскрутки стека: если условие ложно, catch не срабатывает, и стек остаётся нетронутым для отладчика и дампов.

Глобальная обработка

В ASP.NET Core не нужно оборачивать каждый контроллер в try/catch: необработанные исключения ловит middleware (UseExceptionHandler) и возвращает ProblemDetails. Подробнее — в разделе ASP.NET Core. В консольных приложениях — AppDomain.CurrentDomain.UnhandledException для логирования перед падением.

Типичные ошибки

Вопросы на засыпку

Выполнится ли finally, если в try есть return?

Да. finally выполняется после вычисления возвращаемого значения, но до фактического выхода из метода. Не выполнится он только при аварийном завершении процесса (Environment.FailFast, StackOverflowException, убийство процесса).

Можно ли поймать StackOverflowException?

Нет, в .NET Core и новее переполнение стека завершает процесс без возможности обработки. Причина почти всегда — бесконечная рекурсия, исправлять нужно код.

Почему исключения дорогие?

При выбросе собирается стек вызовов, ищутся обработчики, раскручиваются кадры стека, выполняются finally. Это в сотни–тысячи раз дороже обычного возврата значения. Для редких ошибок это неважно, но в горячем цикле или для ожидаемых ситуаций (валидация ввода) лучше TryXxx и возврат результата.

Что такое AggregateException?

Контейнер для нескольких исключений: например, Task.WaitAll или Parallel.ForEach, где упали несколько задач. Внутренние исключения — в InnerExceptions. При await на задаче пробрасывается первое внутреннее исключение, а не обёртка.

Итог

Исключения сообщают об ошибках, которые нельзя обработать на месте: try защищает код, catch обрабатывает конкретные типы от частного к общему, finally и using гарантируют освобождение ресурсов. Перебрасывайте через throw;, оборачивайте с innerException, используйте фильтры when, ловите только то, что можете обработать, и не используйте исключения для обычной логики.