Как обрабатываешь ошибки

Ответ

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

1. Уровень бизнес-логики / сервисов (Try-Catch с конкретными исключениями)

public async Task<OperationResult> ProcessOrderAsync(Order order)
{
    try
    {
        ValidateOrder(order); // Может выбросить ValidationException
        var inventory = await _inventoryService.ReserveAsync(order.ProductId); // Может выбросить InventoryServiceException
        await _paymentGateway.ChargeAsync(order); // Может выбросить PaymentGatewayException
        return OperationResult.Success();
    }
    catch (ValidationException ex) // Ловим максимально конкретное исключение
    {
        _logger.LogWarning(ex, "Validation failed for order {OrderId}", order.Id);
        return OperationResult.Failure("Invalid order data."); // Возвращаем понятный результат клиенту
    }
    catch (InventoryServiceException ex) when (ex.Code == "OUT_OF_STOCK") // Фильтр исключений (C# 6+)
    {
        _logger.LogInformation("Product {ProductId} is out of stock", order.ProductId);
        return OperationResult.Failure("Product is out of stock.");
    }
    catch (Exception ex) // Общий catch — только для логирования и переброса
    {
        _logger.LogError(ex, "Unexpected error processing order {OrderId}", order.Id);
        throw; // Перебрасываем исходное исключение на верхний уровень (Middleware)
    }
}

2. Уровень доступа к данным (Использование using и finally)

public Customer GetCustomer(int id)
{
    using var connection = new SqlConnection(_connectionString); // using гарантирует Dispose
    try
    {
        connection.Open();
        return connection.QuerySingle<Customer>("SELECT * FROM Customers WHERE Id = @Id", new { Id = id });
    }
    catch (SqlException ex) when (ex.Number == 547) // Ошибка FK-ограничения
    {
        throw new BusinessRuleException("Cannot delete referenced customer.", ex);
    }
    // finally не нужен явно, так как using его обеспечивает для освобождения connection
}

3. Глобальный уровень (Middleware в ASP.NET Core)

Перехватывает все необработанные исключения из контроллеров, логирует их и возвращает клиенту стандартизированный ответ (например, ProblemDetails).

// Program.cs
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        _logger.LogError(exception, "Unhandled exception");

        context.Response.StatusCode = exception switch
        {
            ValidationException => StatusCodes.Status400BadRequest,
            NotFoundException => StatusCodes.Status404NotFound,
            _ => StatusCodes.Status500InternalServerError
        };

        await context.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Title = "An error occurred",
            Detail = exception?.Message,
            Status = context.Response.StatusCode
        });
    });
});

Ключевые принципы:

  • Не глотайте исключения (catch {} без действий или throw ex; (перезаписывает стек вызовов)).
  • Создавайте свои исключения, когда стандартных недостаточно (public class DomainException : Exception).
  • Используйте try-catch для восстановления после ожидаемых ошибок (например, некорректный ввод).
  • Позволяйте исключениям всплывать для неожиданных ошибок (например, сбой БД) до глобального обработчика.

Ответ 18+ 🔞

А, обработка ошибок, классика жанра! Слушай, тут всё как в жизни — если где-то можно накосячить, обязательно накосячат. И твоя задача — не просто упасть с дурацкой рожей, а красиво споткнуться, извиниться и пойти дальше, как будто так и надо.

Вот смотри, как это обычно выстраивают, чтоб не получилось говна и палок.

1. Уровень, где вся бизнес-логика творится (сервисы, ядро)

Тут ты уже должен понимать, какая именно хрень может случиться. Не лови всё подряд, как дурак.

public async Task<OperationResult> ProcessOrderAsync(Order order)
{
    try
    {
        ValidateOrder(order); // Может кинуть ValidationException
        var inventory = await _inventoryService.ReserveAsync(order.ProductId); // Может кинуть InventoryServiceException
        await _paymentGateway.ChargeAsync(order); // Может кинуть PaymentGatewayException
        return OperationResult.Success();
    }
    catch (ValidationException ex) // Ловишь конкретно то, что ожидаешь
    {
        _logger.LogWarning(ex, "Валидация заказа {OrderId} провалилась", order.Id);
        return OperationResult.Failure("Данные заказа — полная хуйня."); // Говоришь пользователю человеческим языком
    }
    catch (InventoryServiceException ex) when (ex.Code == "OUT_OF_STOCK") // Во, фильтр! C# 6+, красота!
    {
        _logger.LogInformation("Товар {ProductId} уже весь разобрали, блядь", order.ProductId);
        return OperationResult.Failure("Товара нет на складе, сорян.");
    }
    catch (Exception ex) // А это — на всякий пожарный, для всего неожиданного
    {
        _logger.LogError(ex, "Ни хрена себе! Непонятная ошибка при обработке заказа {OrderId}", order.Id);
        throw; // Кидаешь это дерьмо выше! Пусть разбираются на глобальном уровне.
    }
}

Суть в чём: на этом уровне ты либо исправляешь ситуацию (возвращаешь вменяемый результат), либо поднимаешь панику наверх, если сам не справился. Главное — не молчи! Не глотай исключения, как пирожок, а то потом будешь три часа дебажить, почему ничего не работает.

2. Уровень работы с данными (базы, файлы, сети)

Тут главное — не забыть за собой прибраться, даже если всё пошло по пизде. Ресурсы надо освобождать.

public Customer GetCustomer(int id)
{
    using var connection = new SqlConnection(_connectionString); // using — твой лучший друг. Сам закроет, даже если вылетит исключение.
    try
    {
        connection.Open();
        return connection.QuerySingle<Customer>("SELECT * FROM Customers WHERE Id = @Id", new { Id = id });
    }
    catch (SqlException ex) when (ex.Number == 547) // Конкретный код ошибки, например, нарушение внешнего ключа
    {
        // Превращаешь техническую ошибку БД в ошибку бизнес-логики
        throw new BusinessRuleException("Нельзя удалять этого клиента, на него есть ссылки, мудак.", ex);
    }
    // finally тут не пиши — using сам всё сделает. Не усложняй.
}

Видишь? Поймал специфическую ошибку базы, обернул её в свою, более понятную для остального кода, и выкинул дальше. А подключение закроется само, потому что using. Красота!

3. Глобальный, самый верхний уровень (Middleware в ASP.NET Core)

Это последний рубеж. Сюда всплывает всё, что не поймали ниже. Задача — не дать упасть всему приложению и не показать пользователю голый стектрейс.

// Program.cs
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        _logger.LogError(exception, "Вот это поворот! Необработанное исключение, блядь!"); // Логируем ВСЁ.

        // Маппим тип исключения на HTTP-статус
        context.Response.StatusCode = exception switch
        {
            ValidationException => StatusCodes.Status400BadRequest,
            NotFoundException => StatusCodes.Status404NotFound,
            _ => StatusCodes.Status500InternalServerError // Всё остальное — наша неожиданная проблема.
        };

        // И отправляем клиенту аккуратный, стандартизированный ответ. Без подробностей внутрненней кухни.
        await context.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Title = "Чёт пошло не так",
            Detail = exception?.Message,
            Status = context.Response.StatusCode
        });
    });
});

Итоговые правила, чтоб не вышло говнокода:

  • Не глотай исключения, идиот! Пустой catch {} или catch (Exception) { /* тишина */ } — это прямой путь в ад при отладке.
  • Не уродуй стек вызовов! Используй throw;, а не throw ex;. Первое перекидывает исключение дальше как есть, второе — обнуляет его историю, и ты не поймёшь, откуда ноги растут.
  • Создавай свои исключения для доменных ошибок. OrderCancellationFailedException понятнее, чем просто InvalidOperationException.
  • try-catch — для восстановления. Лови только те ошибки, после которых ты знаешь, что делать дальше. Всё остальное — пусть летит наверх.
  • using — твоя броня. Для всего, что реализует IDisposable. Базы, файлы, потоки. Он тебя спасёт.