Какие проблемы возникают при обработке всех исключений в одном месте без разделения по типам?
Что проверяют
Отличаете ли вы глобальный обработчик как последний рубеж от привычки ловить Exception везде и реагировать одинаково. Следом обычно просят показать, как это устроить в ASP.NET Core: что ловится локально, что уходит наверх и какой статус получает клиент.
Как отвечать
Начните с оговорки: единая точка для необработанных исключений это нормально. В ASP.NET Core это UseExceptionHandler и, начиная с .NET 8, реализации IExceptionHandler, которые превращают исключение в ответ ProblemDetails. Плохо другое, когда все ошибки получают одинаковую реакцию:
- неверные коды ответа: ошибка валидации, отсутствующая сущность и упавшая база превращаются в одинаковый 500. Клиент не отличает «исправьте запрос» от «повторите позже», а ретраи бьют по заведомо безнадёжным запросам;
- отмена как авария:
OperationCanceledExceptionпри разрыве соединения или поCancellationTokenлогируется как ошибка и засоряет алерты; - непонятно, повторять ли: временные сбои (
TimeoutException,HttpRequestException, транзиентные ошибки базы) имеет смысл повторять через Polly илиMicrosoft.Extensions.Http.Resilience, а баги в коде нет; - утечка деталей: общий обработчик часто отдаёт клиенту
ex.Messageи стек; - проглатывание: пустой
catch (Exception) { }в глубине кода прячет баг, и система продолжает работать на испорченном состоянии.
Как надо: локально ловить только то, что вы умеете обработать, и конкретным типом. Для доменных ситуаций заводить свои исключения или возвращать результат вместо исключения, для условий использовать фильтры catch (DbUpdateException ex) when (...). Наверху разбирать тип и выставлять 400, 404, 409 или 500. Пробрасывать через throw;, а не throw ex;, иначе стек начнётся с этой строки.
Где спотыкаются
Предлагают вообще отказаться от глобального обработчика. Он нужен, вопрос в том, чтобы он различал типы, а не был единственным местом логики.
Логируют одно исключение на каждом уровне при пробросе. В логе пять записей об одной ошибке, и настоящую причину приходится искать.
Оборачивают исключение в новое без innerException. Исходный тип и стек теряются, и разделять по типам наверху уже нечего.