Исключения: try, catch, finally, when, свои исключения
↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая
Зачем это на собесе
правильная обработка ошибок отличает надёжный сервис. Спросят про иерархию исключений,
throwvsthrow ex, фильтрыwhen, стоимость исключений и когда их не использовать.
Объяснение
Иерархия
System.Exception— база.SystemException:NullReferenceException,InvalidOperationException,ArgumentException(+ArgumentNullException,ArgumentOutOfRangeException),FormatException,KeyNotFoundException,OperationCanceledException(+TaskCanceledException),TimeoutException,IOException,HttpRequestException.- Свои исключения наследуют от
Exception(не отApplicationException).
try / catch / finally
try
{
var data = await collector.CollectAsync(patientId, ct);
return reportFactory.Create(type).Render(data);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw; // отмена клиентом — не ошибка, пробрасываем
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) // фильтр исключений
{
throw new PatientNotFoundException(patientId, ex); // оборачиваем с InnerException
}
catch (Exception ex)
{
_log.LogError(ex, "Report generation failed for {PatientId}", patientId);
throw;
}
finally
{
stopwatch.Stop(); // выполнится всегда
}- Порядок
catch— от частного к общему. - Фильтр
whenпроверяется до раскрутки стека. Не пойманное исключение сохраняет исходный стек, а фильтр можно использовать для логирования без перехвата:catch (Exception ex) when (Log(ex)) {}сLog, возвращающим false.
throw; vs throw ex;
catch (Exception ex)
{
throw; // сохраняет исходный стектрейс
// throw ex; // стектрейс начинается отсюда — теряем, где реально упало
}Для проброса из другого контекста: ExceptionDispatchInfo.Capture(ex).Throw().
Свои исключения
public sealed class PatientNotFoundException(Guid patientId, Exception? inner = null)
: Exception($"Patient {patientId} not found", inner)
{
public Guid PatientId { get; } = patientId;
}Добавляйте данные для диагностики, передавайте inner. Доменные исключения потом маппятся в HTTP-статусы глобальным обработчиком (PatientNotFoundException → 404).
using и освобождение ресурсов
using — синтаксический сахар над try/finally { Dispose() }:
await using var conn = await dataSource.OpenConnectionAsync(ct);
using var stream = new MemoryStream();Исключения и производительность
Выброс исключения дорогой (сбор стектрейса, раскрутка стека): микросекунды против наносекунд обычного return. Правило:
- исключения — для исключительных ситуаций (нарушение контракта, недоступна БД);
- ожидаемые исходы (валидация пользовательского ввода, «не найдено» в поиске) — через
TryXxx,bool,nullили Result pattern.
Где ловить
- Не ловить «на всякий случай» в каждом методе.
- Ловить там, где можно что-то сделать: повторить, преобразовать в доменную ошибку, добавить контекст, освободить ресурс.
- Всё остальное — в глобальном обработчике (
IExceptionHandler/ middleware), который логирует и возвращает ProblemDetails.
Нюансы и подводные камни
- Пустой
catch {}глотает ошибку — худший антипаттерн. catch (Exception)вокругawaitперехватит иOperationCanceledException: отмену обрабатывайте отдельно.- Исключение в
finallyзаменит исходное. - В
async voidисключение не перехватить снаружи, оно роняет процесс. - Необработанное исключение в
BackgroundServiceв .NET 8+ по умолчанию останавливает хост (BackgroundServiceExceptionBehavior.StopHost). StackOverflowExceptionиOutOfMemoryException(в общем случае) не перехватываются осмысленно.
Тестирование
[Fact]
public async Task Throws_when_patient_missing()
{
var act = () => service.GetAsync(Guid.NewGuid(), CancellationToken.None);
await act.Should().ThrowAsync<PatientNotFoundException>()
.Where(e => e.PatientId != Guid.Empty);
}Вопросы с ответами
Чем throw отличается от throw ex?
throw сохраняет исходный стектрейс. throw ex сбрасывает его на текущую строку, и теряется место, где на самом деле произошла ошибка.
Что такое фильтры исключений (when) и чем они лучше if внутри catch?
Условие проверяется до раскрутки стека. Если оно ложно, исключение не считается пойманным и идёт дальше с неизменённым стеком. Удобно для выборочной обработки и логирования.
Когда использовать исключения, а когда Result или Try-паттерн?
Исключения — для неожиданных ситуаций и нарушения контрактов. Для ожидаемых исходов (валидация, «не найдено», бизнес-отказ) — Result, bool TryXxx или null: это дешевле и явнее в сигнатуре.
Выполнится ли finally, если в try есть return?
Да, finally выполняется при return, break, continue и исключении. Не выполнится только при аварийном завершении процесса (Environment.FailFast, StackOverflow, kill).
Как организовать обработку ошибок в Web API?
Доменные исключения или Result в бизнес-слое. Глобальный IExceptionHandler, который логирует и маппит исключения в ProblemDetails со статусами (404, 409, 422, 500). Без утечки деталей в проде.
Связанные темы
- Предыдущая: Методы и параметры: ref, out, in, params, именованные аргументы · Следующая: Enum, record, tuple, деконструкция
- Глобальная обработка ошибок: Глобальная обработка ошибок: IExceptionHandler, middleware
- Result pattern: Result pattern и обработка ошибок без исключений
- IDisposable: IDisposable, using, финализаторы, IAsyncDisposable
- Ошибки в JS: Обработка ошибок
- async void: Антипаттерны: async void, .Result, sync over async