Где размещать catch и логирование при обработке исключений
· Го Комура · Обработка исключений, Логирование, Обработка ошибок, Проектирование, C# / .NET
Содержание
- Сначала вывод
catch, логирование и обработка ошибок - разные вещи- 2.1. Перехват (
catch) - 2.2. Логирование
- 2.3. Обработка ошибок
- 2.4. Трансляция исключений
- 2.1. Перехват (
- Таблица для первичного решения
- Что делать на каждом уровне цепочки вызовов
- 4.1. Самый глубокий helper / utility / private-метод
- 4.2. Граница внешнего I/O: Repository / Gateway / обёртки SDK
- 4.3. Application Service / UseCase
- 4.4. Граница UI / HTTP / Job / Message
- 4.5. Последний обработчик необработанных исключений
- 4.6. Взгляд на одну цепочку вызовов
- Разделение ожидаемых неудач и непредвиденных исключений
- Где и сколько раз логировать
- Частые ошибки
- Чек-лист для ревью
- Краткая сводная таблица
- Итог
- Справочные материалы
- Похожие статьи
1. Сначала вывод
- Принцип: не перехватывать широко на глубоких слоях. Место
catchсмещается к границе, где можно определить единицу неудачи. - Логирование по умолчанию: один основной лог на одну неудачу. Если каждый слой продолжает логировать одно и то же исключение как
Error, читателю становится тяжело. - Ответственность самого глубокого слоя - уборка, локальный откат, трансляция исключения и, если нужно, ограниченный retry. Если исключение пробрасывается дальше, обычно именно там основной лог не пишется.
- Границы обработки - одна операция на экране, один HTTP-запрос, одно задание, обработка одного сообщения - чаще всего оказываются самым естественным местом для основного лога.
- Ожидаемые неудачи превращаются в результат на уровне соответствующего use case. Не обязательно прокидывать всё до самого верха в виде исключения.
AppDomain.UnhandledException,DispatcherUnhandledExceptionв WPF,ThreadExceptionв WinForms, обработчик исключений ASP.NET Core, финальная обработка исключений на уровне хоста - это скорее последняя точка фиксации, чем точка восстановления.OperationCanceledExceptionиз-за отмены пользователем или завершения работы обычно не считается Error.- Если сомневаетесь, проверяйте в таком порядке:
- Можно ли действительно принять решение в этом месте?
- Известна ли здесь единица неудачи?
- Можно ли здесь откатить или заново собрать состояние?
- Если залогировать здесь, не залогируется ли то же исключение выше?
Иными словами, основа такова: перехватывать не там, где это возможно, а там, где можно ответственно принять решение.
2. catch, логирование и обработка ошибок - разные вещи
2.1. Перехват (catch)
catch - это приём исключения с изменением дальнейшего хода обработки.
Но само по себе это ещё не восстановление.
Например, даже если исключение получено в нижележащем методе, но при этом:
- непонятно, что показать пользователю;
- непонятно, нужно ли останавливать весь экран или достаточно провалить только текущую операцию;
- непонятно, можно ли продолжать этот request или job,
то, скорее всего, это место не подходит для catch.
2.2. Логирование
Лог - это запись не только самого факта «произошло исключение», но и того, какая именно работа не удалась, чтобы потом можно было это отследить.
Поэтому у хорошей точки логирования обычно есть что-то из этого списка:
- requestId / traceId
- userId
- orderId / fileId / batchId
- номер обрабатываемой записи
- какая именно операция на экране
- какая очередь, какое сообщение
Глубокие helper-ы и общие функции часто знают технические детали, но не располагают этим контекстом. Поэтому место, знающее технические детали, и место, знающее рабочий контекст, зачастую оказываются разными.
2.3. Обработка ошибок
Под обработкой ошибок здесь понимается вот что.
- Показ сообщения об ошибке на экране
- Возврат 4xx / 5xx в HTTP
- Провал только одной записи с переходом к следующей
- Повторная инициализация подсистемы
- Завершение процесса с расчётом на перезапуск
- Освобождение ресурсов и безопасный выход
Иными словами, это определение того, как неудача выглядит с точки зрения вызывающей стороны или пользователя.
2.4. Трансляция исключений
На практике между catch и «обработкой» есть ещё одна важная задача.
Это трансляция.
Например, если пропустить напрямую в UI или Controller:
HttpRequestExceptionIOExceptionJsonException- специфичные исключения драйвера БД
- специфичные исключения SDK вендора,
то верхние слои начинают знать о деталях реализации нижних.
Поэтому на границе их преобразуют в неудачи, осмысленные для этого слоя, например:
- «не удалось подключиться к платёжному сервису»;
- «формат CSV повреждён»;
- «не удалось записать в место назначения»;
- «ответ устройства некорректен».
Здесь важно, что трансляция и логирование - не одно и то же. Если исключение просто транслируется и пробрасывается дальше, основной лог обычно не пишется.
3. Таблица для первичного решения
Проще всего сразу определить общую политику по этой таблице.
| Место | Базовая политика | Основной лог | Основная ответственность |
|---|---|---|---|
| helper / utility / private-метод | По умолчанию не перехватывать широко | Не пишется | Уборка через finally, локальный откат, минимальное добавление контекста |
| Repository / Gateway / обёртка SDK | Перехватывать только конкретные исключения | Обычно не пишется | Трансляция исключений, ограниченный retry, отбрасывание соединений и хендлов |
| Application Service / UseCase | Превращать ожидаемые неудачи в результат | При поглощении - здесь, при необходимости | Определение единицы неудачи, частичный провал, решения уровня use case |
| Граница UI / Controller / API / Job / Message | Основной приёмник непредвиденных исключений | Здесь чаще всего основной лог | Ответ пользователю, HTTP-ответ, переход к следующей записи, решение об abort |
| Обработчик необработанных исключений | Последний рубеж против утечек | Critical |
Финальная фиксация, flush, dump, путь завершения / перезапуска |
Схематично это выглядит примерно так.
flowchart TD
A["Произошло исключение"] --> B{"Можно ли в этом месте решить retry / результат / возможность продолжения?"}
B -- "Нет" --> C["По умолчанию не перехватывать, передать выше"]
B -- "Да" --> D{"Это граница слоёв?"}
D -- "Нет" --> E["Только локальная уборка"]
D -- "Да" --> F["При необходимости транслировать в осмысленное исключение"]
E --> G{"Известны ли здесь единица неудачи и рабочий контекст?"}
F --> G
G -- "Нет" --> H["Не писать основной лог, передать выше"]
G -- "Да" --> I["Написать основной лог один раз и решить, каким будет ответ"]
I --> J["При необходимости: завершение / повторная инициализация / переход к следующей записи"]
У этой схемы два ключевых момента.
- Первая причина для
catch- восстановление или уборка, а не логирование. - Первая причина для лога - наличие собранного рабочего контекста, а не сам факт обнаружения исключения.
4. Что делать на каждом уровне цепочки вызовов
4.1. Самый глубокий helper / utility / private-метод
Здесь базовое правило - не перехватывать широко.
Такие места, как преобразование строк, парсинг, вычисления, внутреннее форматирование, общие helper-ы, не могут определить:
- какая это была операция на экране;
- какой это был request;
- допустимо ли провалить только эту попытку;
- нужно ли закрывать весь экран целиком.
На этом слое допустимо в основном следующее.
- Освобождение ресурсов в
finally - Откат локального состояния, изменённого наполовину
- Минимальное добавление контекста к сообщению исключения
- Замена на более подходящий тип исключения
- Уничтожение объектов, которые больше нельзя переиспользовать
И наоборот, следует избегать такого стиля.
catch (Exception)с возвратомnull/false/ пустого массива- Показ
MessageBoxв этом месте - Логирование как
Errorздесь с последующим повторным пробросом - «Просто продолжать», когда состояние нельзя восстановить
Особенно опасен паттерн, при котором сбой происходит после того, как собственное состояние уже частично изменено, а объект продолжают использовать как ни в чём не бывало. В этом случае либо откатывают состояние на месте, если это возможно, либо считают объект одноразовым и подлежащим уничтожению.
4.2. Граница внешнего I/O: Repository / Gateway / обёртки SDK
Здесь причина для catch очевидна.
Потому что именно тут наружу выходят детали реализации нижележащего слоя.
- Исключения драйвера БД
- Исключения HTTP-обмена
- Исключения файлового I/O
- Специфичные исключения COM / P/Invoke / SDK вендора
- Исключения библиотек парсинга и сериализаторов
На этом слое выполняется, по сути, четыре вещи.
-
Перехват конкретных исключений Не широкий
Exception, а конкретные, осмысленные исключения. -
Трансляция в осмысленную неудачу Чтобы верхним слоям не приходилось знать внутренние детали нижних напрямую.
- Если нужен локальный retry - делать его здесь
Но условия строгие:
- известно, что неудача временная;
- операция идемпотентна;
- заданы предел попыток и стратегия ожидания;
- итоговое поведение при провале ясно. Только когда выполняются все четыре условия.
- Отбрасывание повреждённых соединений и хендлов Часто безопаснее «пересоздать соединение», чем «продолжать с тем же объектом».
Политика логирования на этом слое остаётся устойчивой при таком подходе:
- при повторном пробросе выше основной лог обычно не пишется;
- если исключение поглощается здесь и превращается в результат, в этот момент выводятся нужные логи и метрики;
- каждую попытку retry обрабатывают в диапазоне
Debug/Information/Warning, а фиксируют весомее только финальную неудачу.
Этот слой - место трансляции, а не, как правило, место финального решения.
4.3. Application Service / UseCase
Это слой, который решает, «как именно провалится эта конкретная работа».
Например:
- операция сохранения;
- подтверждение заказа;
- импорт CSV;
- обработка одной записи батча;
- применение одного сообщения -
это единицы, целостные как use case, и они находятся здесь.
На этом слое можно принимать такие решения.
- Ошибка валидации - провал только этой попытки
NotFoundсоответствует 404- Нарушение бизнес-правила - ожидание исправления пользователем
- Некорректная строка CSV логируется как
Warning, обработка продолжается - Временный сбой внешнего сервиса приводит к провалу всей операции
- Промежуточный результат отбрасывается, обработка начинается заново
Иными словами, это место, где можно определить единицу неудачи.
Этому слою подходит такая работа:
- превращение ожидаемых неудач в
Resultили DTO неудачи; - агрегация частичных провалов;
- определение, сколько неудач допустимо перед остановкой;
- преобразование в коды ошибок или ключи сообщений для пользователя.
И наоборот, этому слою не стоит тащить на себя слишком много отображения UI или формирования тела HTTP-ответа. Здесь легче разделить ответственность, если слой определяет только смысл на уровне use case, а окончательное представление отдаёт границе.
4.4. Граница UI / HTTP / Job / Message
Именно здесь в большинстве приложений чаще всего оказывается точка основного лога.
Например, такие единицы:
- одно нажатие кнопки «Сохранить» в WinForms / WPF;
- один HTTP-запрос в ASP.NET Core;
- одно сообщение worker-а;
- одна запись входных данных в батче;
- один запуск запланированного задания.
Это место знает:
- какая была операция;
- чья это была операция;
- какая это по счёту запись;
- какой request / batch / message;
- что вернуть пользователю или вызывающей стороне при неудаче.
Поэтому здесь обычно берут на себя такие роли:
- принимать непредвиденные исключения в одном месте;
- писать основной лог один раз, с контекстом;
- преобразовывать в диалог с ошибкой, HTTP 500, Problem Details, провал задания, переход к следующей записи и т. п.
Важно на этом слое не само по себе широкое перехватывание, а то, определено ли, что вернуть после такого перехвата.
Например, для батчей и очередей полезно мыслить в два этапа.
- Перехват на границе одной записи Решение, допустимо ли провалить только эту запись и перейти к следующей
- Родительский цикл не должен глушить всё подряд Если родительский цикл падает, лучше склоняться к перезапуску всего процесса
«Проваливать записи по одной и продолжать» и «родительский цикл молча живёт дальше после непредвиденного исключения» - совершенно разные вещи.
4.5. Последний обработчик необработанных исключений
Это последний рубеж. Не волшебная точка восстановления.
Типичные представители:
AppDomain.UnhandledException;Application.DispatcherUnhandledExceptionв WPF;Application.ThreadExceptionв WinForms;- middleware и обработчики исключений ASP.NET Core;
- финальная обработка исключений в Generic Host / worker /
BackgroundService.
Основная ответственность этого слоя - самое большее следующее.
- Финальное логирование
- Flush
- Обеспечение пути сбора dump
- Сохранение информации о сессии и недавнего контекста
- Подготовка кода завершения и пути перезапуска
С другой стороны, не стоит возлагать на него слишком много ожиданий.
- К моменту, когда исключение дошло сюда, это чаще всего проектный пробел выше по цепочке;
- состояние уже могло быть повреждено;
- возможно удержание блокировок, из-за чего тяжёлая работа здесь опасна;
- даже если внешне можно продолжить, это не значит, что продолжать безопасно.
Есть и практические нюансы, важные именно для .NET.
AppDomain.UnhandledException- это событие для уведомления и фиксации необработанных исключений. Закладывать в него слишком много логики восстановления опасно.- В
DispatcherUnhandledExceptionWPF есть путь установитьHandled = trueи внешне продолжить работу, но сначала нужно определить, возможно ли восстановление. ThreadExceptionв WinForms тоже может оставить приложение в неизвестном состоянии после обработки.- Middleware обработки исключений ASP.NET Core нужно размещать ближе к началу конвейера, чтобы оно могло перехватывать исключения из последующих этапов.
- Необработанное исключение в
BackgroundService, начиная с .NET 6, логируется и по умолчанию приводит к остановке хоста. Иногда безопаснее остановиться и положиться на стратегию перезапуска, чем глушить всё в родительском цикле.
Особенно в десктопных приложениях существует путь «поймать необработанное исключение и продолжить работу». Но возможность продолжить и правильность продолжения - разные вещи.
4.6. Взгляд на одну цепочку вызовов
Рассмотрим, например, такой поток.
flowchart LR
A["Граница UI / Controller / Job"] --> B["Application Service / UseCase"]
B --> C["Domain / бизнес-логика"]
C --> D["Repository / Gateway / обёртка SDK"]
D --> E["БД / HTTP / файл / SDK вендора"]
Роли при этом распределяются примерно так.
Кнопка «Сохранить» → SaveOrderUseCase → PaymentGateway → HTTP
PaymentGateway- принимает сбои соединения и некорректный формат ответа;
- транслирует их в «не удалось подключиться к платёжному сервису» / «некорректный ответ платёжного сервиса»;
- если нужен retry, выполняет его здесь, при определённых условиях;
- при повторном пробросе обычно не пишет основной лог.
SaveOrderUseCase- превращает ожидаемые неудачи вроде отказа в платеже в результат;
- трактует это как «провал только текущего подтверждения заказа»;
- оформляет результат неудачи так, чтобы его удобно было вернуть в UI или API.
- Обработчик кнопки UI / Controller
- принимает непредвиденные исключения в одном месте;
- пишет основной лог с
orderId,userId,requestId; - преобразует его в диалоговое окно или ответ 500 / 503.
- Обработчик необработанных исключений
- фиксирует только то, что дошло до этой точки;
- выполняет dump и финальный flush;
- ставит в приоритет путь завершения, а не восстановление.
При таком разделении получается форма, при которой технические детали закрываются внизу, рабочий контекст добавляется наверху, а решения принимаются на границе.
5. Разделение ожидаемых неудач и непредвиденных исключений
Самое важное в этой теме - не относиться ко всему как к одному и тому же «исключению».
Сначала разделим так.
| Вид неудачи | Первое место обработки | Типичная трактовка |
|---|---|---|
| Недочёт валидации | Граница UseCase / request | Возвращается как ошибка ввода |
NotFound / Conflict |
UseCase / Controller | 404 / 409 или сообщение на экране |
| Отмена пользователем / завершение работы | Граница операции | Трактуется как отмена. Обычно не Error |
| Некорректная строка CSV | Граница записи | Фиксируется как Warning, обработка продолжается |
| Временный timeout, в итоге приводящий к провалу | Граница I/O - граница request | Возвращается как неудача после retry |
NullReferenceException, нарушение инварианта |
Граница request / job | Основной лог и ответ с ошибкой |
AccessViolationException, серьёзный OutOfMemoryException, признаки повреждения на native-границе |
Финальная граница | Critical, склонность к завершению |
Ожидаемые неудачи - это неудачи, которые можно заранее предусмотреть на этапе проектирования. Непредвиденные исключения - это неудачи, после которых сомнительно, можно ли доверять состоянию дальше.
Само это разделение снижает количество таких инцидентов:
NotFoundкаждый раз логируется какError;- отмена пользователем трактуется как сбой;
- действительно опасное нарушение инварианта проскальзывает как «провал только на этот раз».
6. Где и сколько раз логировать
В проектировании логирования важнее заранее определить, кто пишет основной лог, чем то, где именно стоит catch.
Базовых правил шесть.
- На одну неудачу - один основной лог
Error/Critical - Нижние слои при необходимости занимаются трансляцией и добавлением контекста
- Верхняя граница пишет основной лог с единицей неудачи и рабочим контекстом
- Ответственность за фиксацию поглощённой неудачи несёт только тот слой, который её поглотил
- Ожидаемые неудачи не логируются как
Errorкаждый раз OperationCanceledExceptionотделяется от обычных логов сбоев
Ниже - примерная таблица точек логирования.
| Ситуация | Основное место логирования | Ориентировочный уровень | Примечание |
|---|---|---|---|
| Ошибка валидации | Граница request / use case | Information или без лога |
Это контрактная неудача, а не сбой |
| Отмена пользователем / shutdown | Граница операции | Debug / Information |
Обычно не Error |
| Временный сбой во время retry | Слой, владеющий retry | Debug / Warning |
Не шуметь до финальной неудачи |
| Retry исчерпан, провал | Граница request / job или слой, поглощающий исключение | Warning / Error |
Фиксировать с единицей неудачи |
| Непредвиденное исключение, роняющее весь request | Граница request / UI / job | Error |
Добавить requestId, userId, entityId |
| Класс завершения процесса | Граница необработанных исключений | Critical |
flush, dump, путь перезапуска |
На практике довольно часто встречается такое дублирование логов:
- Repository логирует
Error; - Service логирует то же исключение как
Error; - Controller снова логирует
Error; - финальный обработчик необработанных исключений логирует ещё и
Critical.
При таком подходе один сбой порождает несколько одинаковых стек-трейсов подряд. Читателю нужны не четыре копии одного и того же стек-трейса, а один основной лог и, при необходимости, немного вспомогательных.
Иначе говоря, базовое правило: логировать один раз, с ровно тем контекстом, который нужен.
7. Частые ошибки
7.1. catch (Exception) глубоко в коде с возвратом null / false
Это легко теряет информацию о причине. Более того, вызывающая сторона перестаёт различать: «данных действительно не было» или «что-то сломалось по пути».
7.2. Логирование Error на каждом слое перед повторным пробросом
Самая частая причина дублирующихся логов.
- нижние слои занимаются только трансляцией;
- верхняя граница пишет основной лог.
При таком разделении дублирование заметно снижается.
При повторном пробросе в C# базовое правило - использовать throw;, чтобы не сломать стек вызовов.
7.3. Библиотечный слой или общий компонент напрямую показывает UI
Если общий компонент показывает MessageBox или напрямую формирует тело HTTP-ответа, страдают и переиспользуемость, и разделение ответственности.
Нижние слои безопаснее ограничить задачей вернуть осмысленную неудачу.
7.4. Логирование OperationCanceledException как сбоя на уровне Error
Отмена - часть управления потоком выполнения.
Если логировать её как Error каждый раз, реальные сбои теряются в шуме.
7.5. Легкомысленный retry при наличии внешних побочных эффектов
Отправка почты, списание платежа, команды устройству, перемещение файлов - многие операции опасно выполнять повторно. Retry уместен только тогда, когда видны сразу оба свойства: временность неудачи и идемпотентность.
7.6. Попытка восстановить всё в финальном обработчике необработанных исключений
Это последняя страховка. Не то место, вокруг которого стоит строить архитектуру.
Стратегию восстановления безопаснее располагать раньше - на границе request / job / подсистемы.
8. Чек-лист для ревью
При ревью обработки исключений полезно проверять в таком порядке - так меньше шансов что-то упустить.
- Можно ли одной фразой сказать, какое решение призван принять этот
catch? - Действительно ли в этом месте можно решить вопрос retry / результата / возможности продолжения / ответа пользователю?
- Если залогировать здесь, не залогируется ли та же неудача как
Errorвыше? - Транслируются ли специфичные для нижней реализации исключения в осмысленную неудачу на границе?
- Можно ли здесь откатить состояние, повреждённое на середине? Если нет - трактуется ли объект как одноразовый?
- Отделён ли
OperationCanceledExceptionот обычных сбоев? - Ясно ли, идёт ли речь о продолжении по записям, провале на уровне request или завершении процесса?
- Ожидается ли от финального обработчика необработанных исключений фиксация, а не восстановление?
- Несёт ли лог контекст единицы неудачи - requestId / userId / batchId / fileId / rowNumber?
- Не трактуются ли «ожидаемая неудача» и «нарушение инварианта» одинаково?
Особенно эффективно в этом чек-листе каждый раз формулировать словами, что именно решает данный catch.
Если на этот вопрос нет ответа, catch, как правило, либо не нужен, либо расположен слишком глубоко.
9. Краткая сводная таблица
Напоследок - предельно сжатая таблица.
| Ситуация | catch |
Лог | Обработка ошибок |
|---|---|---|---|
| helper / utility | По умолчанию нет | Нет | Нет |
| Repository / Gateway / обёртка SDK | Только конкретные исключения | Обычно без основного лога | Трансляция, локальный retry, отбрасывание соединений |
| UseCase / Application Service | Принимает ожидаемые неудачи | При поглощении - по необходимости | Формирование результата, частичный провал |
| Граница UI / Controller / request / item / job | Широко принимает непредвиденные исключения | Основной лог | Ответ, сообщение, продолжение / abort |
| Обработчик необработанных исключений | Только то, что дошло сюда | Critical |
Финальная фиксация, путь завершения |
Если сомневаетесь, достаточно этих пяти пунктов.
- На глубоких слоях широко не хватать
- Перехватывать на границах
- Основной лог - один раз
- Ответственность несёт слой, поглотивший исключение
- Последнее необработанное исключение - это фиксация и путь завершения
10. Итог
Обработка исключений - это не история про «раз перехватить можно везде, значит и перехватываем везде».
Порядок проверки в целом такой, и его достаточно.
- Можно ли действительно принять решение в этом месте?
- Известна ли здесь единица неудачи?
- Можно ли здесь откатить или заново собрать состояние?
- Не приведёт ли логирование здесь к дублированию?
- Это точка восстановления или последняя точка фиксации?
При такой последовательности проверки цепочку вызовов становится заметно легче упорядочить.
Особенно важны три вещи.
- Глубокие слои - в основном трансляция и уборка
- Границы - в основном решения и основной лог
- Финальный обработчик необработанных исключений - в основном фиксация и путь завершения
Иначе говоря, базовое правило таково: исключения принимаются на границе, снабжаются контекстом и обрабатываются только там, где возможно восстановление.
Как только это определено, и код-ревью, и расследование инцидентов становятся заметно более предсказуемыми.
11. Справочные материалы
- .NET: рекомендации по работе с исключениями
- .NET: событие System.AppDomain.UnhandledException
- WPF: событие Application.DispatcherUnhandledException
- Windows Forms: событие Application.ThreadException
- Обработка ошибок в ASP.NET Core
- Middleware в ASP.NET Core
- Службы Windows на основе BackgroundService
12. Похожие статьи
- Чек-лист на случай непредвиденного исключения - завершать приложение или продолжать? Таблица для первичного решения
- Если без собственного logger не обойтись, каковы по-настоящему необходимые минимальные требования: практические требования и взгляд на интеграционное тестирование
- Что такое Generic Host в .NET - сначала разбираемся с DI, конфигурацией, логированием и BackgroundService
- Где заканчивается зона модульных тестов и начинается зона интеграционных - как проводить границу и практическая таблица решений
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Таблица решений: завершать работу приложения или продолжать при неожиданном исключении
Разбираем, когда после неожиданного исключения приложение стоит завершать, а когда можно продолжать работу — с точки зрения повреждения с...
Проектирование Windows-приложений, сохраняющих логи и дампы при сбое
Разбираем, как сочетать обычное логирование, финальный маркер сбоя, WER LocalDumps и процесс-наблюдатель, чтобы даже при падении Windows-...
Минимальный чек-лист безопасности при разработке Windows-приложений
Систематизируем в виде чек-листа базовые меры безопасности для бизнес-приложений на WPF / WinForms / WinUI / C++ / C#: права доступа, под...
Обработка ошибок и повторные попытки в Power Automate — как не допустить, чтобы работающий поток незаметно остановился
Сборник паттернов проектирования, которые не дают потокам Power Automate незаметно останавливаться: значения политики повторов по умолчан...
Обработка ошибок и повторные попытки в PowerShell — от ловушки нерабочего try/catch до exit code и типовых схем повтора
Разбираем на практике различия между завершающими и незавершающими ошибками в PowerShell, ловушку неработающего try/catch и приём -ErrorA...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- На каком слое следует перехватывать исключения?
- Принцип таков: не перехватывать широко на глубоких слоях, а смещать catch к границе, где можно определить единицу неудачи. Естественными точками приёма служат границы обработки: одна операция на экране, один HTTP-запрос, одно задание, одно сообщение. Основа - перехватывать не там, где это возможно технически, а там, где можно ответственно решить вопрос retry, результата или возможности продолжения. В глубоких helper-ах и утилитах ограничиваются уборкой в finally, локальным откатом и трансляцией исключения.
- Нужно ли логировать исключение на каждом слое?
- На одну неудачу основной лог уровня Error / Critical должен быть один. Если Repository логирует Error, Service логирует то же исключение как Error, а Controller делает то же самое ещё раз, при одном сбое получается несколько одинаковых стек-трейсов подряд, и читателю становится тяжело. Нижние слои должны ограничиваться трансляцией и добавлением контекста, а основной лог пишется на верхней границе, где собран рабочий контекст вроде requestId и userId. Ответственность за фиксацию неудачи несёт только тот слой, который поглощает исключение и превращает его в результат.
- Как разграничить ожидаемые неудачи и непредвиденные исключения?
- Ожидаемые неудачи - это те, что можно заранее предусмотреть на этапе проектирования: ошибки валидации или NotFound превращаются в результат на уровне use case и обычно не логируются как Error при каждом случае. OperationCanceledException из-за отмены пользователем тоже, как правило, не считается Error. А вот нарушение инварианта вроде NullReferenceException стоит основным логом фиксировать на границе request / job и превращать в ответ с ошибкой, а AccessViolationException или серьёзный OutOfMemoryException обрабатывать как Critical, склоняясь к завершению работы. Само это разделение заметно снижает риск того, что действительно опасная неудача останется незамеченной.
- Что должен делать обработчик необработанных исключений?
- AppDomain.UnhandledException, DispatcherUnhandledException в WPF, ThreadException в WinForms - это не точка восстановления, а последняя точка фиксации. Основная задача здесь - финальное логирование, flush, обеспечение сбора dump, подготовка кода завершения и пути перезапуска. К этому моменту состояние уже могло быть повреждено, поэтому даже видимость продолжения работы не означает, что продолжать действительно безопасно. Стратегию восстановления безопаснее располагать раньше - на границе предыдущего request или job.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки