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