Исключения: try, catch, finally, when, свои исключения

↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая

Обязательно~6 мин чтенияУровень: junior

Зачем это на собесе

правильная обработка ошибок отличает надёжный сервис. Спросят про иерархию исключений, throw vs throw 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);
}

Вопросы с ответами

Связанные темы