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

«Как обрабатываешь ошибки» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Обработка ошибок строится на многоуровневой стратегии, где каждый уровень отвечает за свой тип ошибок. Основной механизм — исключения (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 для восстановления после ожидаемых ошибок (например, некорректный ввод).
  • Позволяйте исключениям всплывать для неожиданных ошибок (например, сбой БД) до глобального обработчика.