Таблица решений: завершать работу приложения или продолжать при неожиданном исключении

· · Разработка Windows, Обработка исключений, Проектирование, C# / .NET, Надёжность

Скачать Excel-чек-лист с японским и английским листами

Когда заходит речь о неожиданных исключениях, легко скатиться к бинарному выбору: «завершить работу или перехватить и продолжить». Но на практике такая постановка вопроса слегка грубовата.

На самом деле важно понять, можно ли ограничить область возможного повреждения.

  • Можно ли завершить неудачей только эту операцию и на этом остановиться?
  • Достаточно ли переинициализировать только этот экран / соединение / worker?
  • Или под сомнением уже целостность всего процесса?

Если смотреть в этом порядке, разобраться становится значительно проще.

В этой статье, ориентируясь на Windows-приложения на C# / .NET, резидентные приложения, службы Windows и инструменты интеграции с оборудованием, мы сводим в таблицу решений условия, при которых после неожиданного исключения можно продолжать работу, и условия, при которых лучше завершить приложение.

1. Сначала — вывод

  • Перехват всего подряд через catch (Exception) с продолжением работы в большинстве случаев опасен.
  • Продолжать можно только тогда, когда одновременно выполняются три условия: неудавшуюся единицу работы можно отбросить, общее состояние можно восстановить и внешние побочные эффекты можно объяснить.
  • Если граница обработки чёткая — одна операция UI, одна входная запись, одно задание, — продолжение работы иногда возможно.
  • И наоборот, если затронуты общее изменяемое состояние, резидентные циклы, главный поток, код запуска, границы с нативным кодом или есть признаки повреждения памяти, — стоит склоняться к завершению.
  • Исключения, ставящие под сомнение работоспособность всего процессаStackOverflowException, AccessViolationException, OutOfMemoryException, — безопаснее не рассматривать как то, после чего можно продолжать работу.
  • В WPF и Windows Forms есть способы перехватить необработанные исключения и внешне продолжить работу, но возможность продолжить и безопасность продолжения — разные вещи.
  • Для долго работающих служб и приложений мониторинга падение с последующим перезапуском зачастую безопаснее и проще диагностировать, чем работа в полуразрушенном состоянии.

Иными словами, критерий решения — можно ли восстановить инварианты.

2. Что в этой статье понимается под «неожиданным исключением»

2.1 Разделяем ожидаемое и неожиданное

Прежде всего, редкое исключение и неожиданное исключение — не одно и то же.

Например, следующее можно считать ожидаемым, даже если это случается нечасто:

  • пользователь выбрал несуществующий файл;
  • удалённый узел временно не ответил по тайм-ауту;
  • одна строка импортируемого CSV оказалась повреждена;
  • при отмене операции возникло исключение OperationCanceledException;
  • нарушение бизнес-правила, из-за которого нужно завершить неудачей только эту операцию.

Это тот тип сбоев, для которых обработку можно заранее определить на этапе проектирования.

С другой стороны, неожиданные исключения, о которых в основном идёт речь в этой статье, выглядят так:

  • нарушилось предположение в собственном коде, и возникло NullReferenceException или InvalidOperationException;
  • исключение вылетело в процессе обновления общего состояния, и непонятно, насколько далеко оно успело примениться;
  • упал родительский цикл цикла мониторинга или обработки сообщений;
  • произошла аномалия на границе COM / P/Invoke / SDK стороннего производителя;
  • сам процесс не проходит проверку работоспособности, как в случае с AccessViolationException или StackOverflowException.

Иначе говоря, это случаи, когда «после этого исключения непонятно, можно ли ещё доверять состоянию приложения».

2.2 Кажется, что выбор из двух, но на самом деле их три

Этот разговор запутывает то, что «продолжение» рассматривают как один-единственный вариант.

На практике оно обычно распадается на три уровня.

Выбор Значение
Завершить неудачей только эту операцию и продолжить Экран остаётся, но именно это сохранение или импорт считается неудавшимся
Остановить только подсистему и продолжить Переинициализировать только соединение, экран, worker или дочерний процесс
Завершить процесс Область повреждения состояния не поддаётся оценке, поэтому предполагается перезапуск

Фраза «приложение продолжает работу» может означать как продолжение как ни в чём не бывало, так и продолжение после изоляции повреждённой части — а это вещи разного веса.

3. Таблица решений, на которую стоит смотреть в первую очередь

3.1 Общая картина

Начав с этой таблицы, обычно можно определить общее направление.

Ситуация Первичный выбор Причина
Неудачей завершились только один ввод, одна операция на экране или одно задание, и их состояние можно отбросить Склоняться к продолжению Неудавшуюся единицу можно локализовать
После исключения затронутый объект или соединение можно уничтожить и пересоздать Склоняться к переинициализации подсистемы Повреждённую область можно локализовать
Общее состояние обновлено частично, и непонятно, насколько далеко это применилось Склоняться к завершению Возможно нарушение инвариантов
Внешние побочные эффекты — БД / файлы / команды устройствам — выполнены наполовину, и нельзя объяснить дублирование или потерю данных Склоняться к завершению Согласованность с внешним миром не поддаётся оценке
Цикл мониторинга, цикл переподключения или родительский цикл обработки сообщений упал из-за неожиданного исключения Склоняться к завершению Молчаливая гибель части функциональности легко превращается в «зомби»-процесс
Сбой произошёл в коде запуска, загрузке конфигурации, композиции DI или инициализации обязательной зависимости Склоняться к завершению как к сбою запуска Частичный запуск опаснее
AccessViolationException, StackOverflowException, серьёзный OutOfMemoryException или признаки повреждения на стороне нативного кода Склоняться к немедленному завершению Под вопросом работоспособность всего процесса
Опасная обработка изолирована в отдельном процессе, а родительский процесс не затронут Родитель продолжает работу, дочерний процесс перезапускается Область отказа уже изолирована
ДаНетНетДаНетДаНетДаНеожиданное исключениеЕсть признаки повреждения памяти / исчерпания стека / критического исчерпания ресурсов?Завершение / FailFast / перезапускМожно ли отбросить неудавшуюся единицу работы?Склоняться к завершениюМожно ли откатить / переинициализировать общее состояние?Остановить подсистему или завершить работуМожно ли объяснить внешние побочные эффекты?Продолжить, считая неудачной только эту операцию

3.2 Что смотреть раньше, чем тип исключения

Не стоит принимать решение сразу по одному только типу исключения. Сначала лучше проверить следующее.

На что смотреть Что проверять
Где произошло Событие UI, одно задание, родительский цикл, код запуска или граница с нативным кодом
Насколько далеко продвинулось Изменилось ли по пути состояние в памяти, БД, файлы или состояние устройства
Возможная область поражения Только этот объект, весь экран или весь процесс
Возможен ли откат Можно ли уничтожить и пересоздать, откатить транзакцией
Внешние побочные эффекты Отправлено или нет, безопасно ли двойное выполнение, возможна ли компенсация
Мониторинг / перезапуск Есть ли автоматический перезапуск или путь восстановления после завершения

3.3 Особо опасные исключения

Разбирать все типы исключений по отдельности не нужно, но есть такие, которые не стоит рассматривать как повод для продолжения работы.

Исключение / признак Первичный выбор Почему это важно
StackOverflowException Склоняться к немедленному завершению Стек вызовов уже разрушен, обычное восстановление предполагать сложно
AccessViolationException Склоняться к немедленному завершению Недопустимый доступ к защищённой памяти, подозрение на границу с нативным кодом или повреждение памяти
OutOfMemoryException Склоняться к завершению Код восстановления, которому самому нужны дополнительные выделения памяти, легко становится нестабильным
Неожиданный NullReferenceException / InvalidOperationException Зависит от контекста, но склоняться к завершению Нарушено собственное предположение кода, возможны незавершённые изменения
Неожиданное исключение, выпавшее из родительского цикла Склоняться к завершению Ядро функциональности мертво, а процесс рискует остаться в живых
Аномалии, возникшие на границе COM / P/Invoke / callback-функций сторонних SDK От немедленного до решительного завершения Оценить безопасность, глядя только на managed-код, сложно

4. Решение в зависимости от того, где это произошло

4.1 События UI

У событий UI — клика по кнопке, перехода между экранами, поиска, выбора файла — сравнительно много пространства для продолжения работы. Однако есть условия.

Продолжение проще в таких случаях:

  • сбой произошёл до загрузки, и бизнес-состояние ещё не затронуто;
  • повреждено только временное состояние внутри диалога, и закрытие диалога его отбрасывает;
  • ViewModel или соединение можно пересоздать после исключения;
  • пользователю можно честно сообщить: «эта операция не удалась».

И наоборот, ситуация склоняется к завершению, когда:

  • частично обновлены и экран, и доменное состояние;
  • затронуто общее состояние, видимое другим экранам — static / singleton / кэши;
  • после исключения остались активность кнопки или состояние выбора, и согласованность непонятна;
  • на UI-потоке возникло неожиданное исключение, и непонятно, насколько далеко продвинулись отрисовка или уведомления.

4.2 Задания / запросы, обрабатываемые по одному

Здесь граница, где продолжение работы даётся легко.

  • Одно сообщение
  • Один файл
  • Один HTTP-запрос
  • Одно задание импорта
  • Один элемент пакетной обработки

Если такие единицы чётко определены, можно завершить неудачей только этот один элемент и перейти к следующему.

Однако есть предпосылки:

  • единица неудачи ясна извне;
  • частичные изменения приводятся в порядок транзакциями или компенсирующими действиями;
  • повторный запуск той же обработки не портит результат;
  • неудачу можно направить в карантинную очередь или журнал ошибок.

4.3 Резидентные циклы / мониторинг / обработка очередей

Это то место, где неаккуратное продолжение работы наиболее опасно.

Например:

  • циклы переподключения;
  • циклы мониторинга;
  • циклы потребления очереди;
  • периодический опрос;
  • мониторинг состояния устройства;
  • резидентная обработка в приложении из области уведомлений (tray).

Страшный сценарий здесь — когда родительский цикл гибнет от одного неожиданного исключения, а процесс продолжает жить сам по себе.

Здесь стоит разделить политику:

  • перехватывать ожидаемые исключения на границе обработки каждого элемента;
  • если неожиданное исключение выпадает из родительского цикла, склоняться к завершению процесса.

4.4 Обработка запуска

Если относиться к сбою при запуске по принципу «сначала как-нибудь запустимся, а там разберёмся», это почти всегда потом аукается.

  • не читаются обязательные настройки;
  • не удалась миграция версии;
  • отсутствует обязательная папка или сертификат;
  • не удалась инициализация ключевой службы;
  • нарушена конфигурация зависимостей.

В таких случаях понятнее завершить работу как сбой запуска.

4.5 Граница с нативным кодом / COM / P/Invoke / unsafe

Здесь стоит смотреть отдельно и несколько строже.

  • COM
  • P/Invoke
  • код за пределами C++/CLI
  • SDK сторонних производителей
  • нативный код, возвращающийся через callback
  • обработка с использованием unsafe

Особенно стоит склоняться к завершению, если видно следующее:

  • AccessViolationException;
  • признаки повреждения кучи или двойного освобождения памяти (double free);
  • аномалии дескрипторов, признаки обращения к освобождённой памяти (use-after-free);
  • внезапная гибель на границе callback.

5. Условия, при которых можно продолжать работу

Сведём условия, при которых продолжение допустимо. Предпосылка — что они выполняются практически все одновременно.

Условие Значение
Единица неудачи ясна Понятно, что отбросить: одну операцию, один экран, одно задание, одно соединение
Состояние можно отбросить Можно уничтожить и пересоздать либо считать неприменённым
Общее состояние защищено Загрязнение не распространяется на другую функциональность
Внешние побочные эффекты можно объяснить Понятно, отправлено / не отправлено / можно ли повторно отправить
Можно быть честным с пользователем Можно показать сообщение «эта операция не удалась»
Есть наблюдаемость По логам, метрикам и дампам возможно последующее расследование

6. Условия, при которых лучше завершить работу

И наоборот, если применимо что-то из следующего, стоит склоняться к завершению.

  • непонятно, что именно было изменено по пути;
  • затронуто общее изменяемое состояние, и согласованность не поддаётся оценке;
  • нарушено управление временем жизни блокировок, очередей, потоков или циклов мониторинга;
  • нельзя объяснить дублирование, потерю или незавершённость внешних побочных эффектов;
  • не удался запуск или инициализация ключевой инфраструктуры;
  • есть подозрение на границу с нативным кодом или повреждение памяти.

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

7. Рекомендации по типичным сценариям

Сценарий Рекомендация Причина
В кнопке открытия файла указан несуществующий путь Продолжить, завершив неудачей только эту операцию Повреждение состояния локально
Повреждена только одна строка при импорте CSV Продолжить, завершив неудачей одну строку или один файл Единицу неудачи легко локализовать
Во время сохранения экрана возникло неожиданное NullReferenceException От пересоздания экрана до завершения работы Непонятно, насколько далеко изменились ViewModel / бизнес-состояние
Одно сообщение из очереди нарушило бизнес-правило Продолжить, завершив неудачей только это сообщение Его можно направить в карантинную очередь
Родительский цикл потребления очереди упал из-за неожиданного исключения Склоняться к завершению процесса Нарушено время жизни всего worker’а
При запуске не читаются обязательные настройки Завершить как сбой запуска Частичный запуск опаснее
AccessViolationException в районе callback-функции стороннего SDK Склоняться к немедленному завершению Нельзя игнорировать возможность повреждения памяти
Не удалась только несущественная отправка телеметрии Отключить только эту функцию и продолжить Область отказа отделима от основной функциональности

8. Типичные ошибки

8.1 catch (Exception) с записью в лог и продолжением работы

Это довольно опасно. Причина скрывается, а повреждённое состояние легко продолжает жить.

8.2 Попытка восстановления в последнем обработчике необработанных исключений

AppDomain.UnhandledException, Application.ThreadException, DispatcherUnhandledException и подобные полезны как место последней записи информации, но не являются волшебной точкой восстановления.

8.3 Беспечный retry при наличии внешних побочных эффектов

Если повторять команды устройствам, отправку писем, списание платежей, перемещение файлов или обновления БД без гарантии безопасности повторного выполнения, на первый план выходит уже инцидент двойного выполнения.

8.4 UI остаётся работать, хотя цикл мониторинга уже мёртв

Приложение, которое выглядит живым, но не выполняет работу, доставляет немало неудобств.

8.5 Заявление «мы не хотим падений» без проектирования под падения

Если не хочется, чтобы приложение падало, для этого сначала нужно подготовить кое-что:

  • автоматический перезапуск;
  • восстановление сеанса;
  • сохранение промежуточных результатов;
  • безопасность повторного выполнения;
  • изоляцию области отказа.

9. Моменты, которые стоит продумать при реализации

9.1 Смещать места catch к границам

Вместо того чтобы перехватывать всё подряд в глубоких слоях, проще навести порядок, если принимать исключения там, где можно определить единицу неудачи:

  • на границе операции UI;
  • на границе одного запроса;
  • на границе одного задания;
  • на границе одного соединения;
  • на границе процесса.

9.2 Разделять ожидаемые и неожиданные исключения

  • Ожидаемые: валидация, «не найдено», тайм-аут, отмена, нарушение бизнес-правил.
  • Неожиданные: нарушение предположений, выпадение из родительского цикла, аномалии на границе с нативным кодом, признаки повреждения памяти.

9.3 Уменьшать общее состояние

Чем больше общее изменяемое состояние, тем сложнее решение о продолжении работы. И наоборот, чем сильнее удаётся замкнуть его внутри одного экрана, одной сессии, одного worker’а, тем легче локализовать и неудачу.

9.4 Выносить опасную обработку в отдельный процесс

Для всего, для чего не хочется распространения ущерба при падении — COM / ActiveX / SDK сторонних производителей / unsafe-код / тяжёлая обработка изображений / управление внешними устройствами, — вынос в отдельный процесс даёт заметный эффект.

9.5 Обработчики необработанных исключений — для «записи», а не «восстановления»

  • сведения об исключении;
  • контекст операции;
  • важные записи в логе непосредственно перед падением;
  • конфигурацию / версию / адреса подключения;
  • механизм сбора дампов.

Подготовив всё это заранее и сделав приоритетом возможность разобраться уже после падения, вы в итоге получите более стабильную систему.

9.6 Не переоценивать события необработанных исключений в WPF / WinForms

В WPF, установив Handled = true в DispatcherUnhandledException, действительно можно продолжить работу после необработанного исключения. В Windows Forms на главном UI-потоке Application.ThreadException и настройка SetUnhandledExceptionMode также позволяют выбирать способ остановки.

Но то, можно ли продолжить работу, и то, выполнены ли условия для восстановления, — разные вопросы.

10. Итог

При возникновении неожиданного исключения важно смотреть не на то, «можно ли перехватить это исключение», а на то, можно ли после этого по-прежнему доверять состоянию приложения.

В качестве порядка принятия решения обычно достаточно следующего:

  1. Можно ли отбросить неудавшуюся единицу работы?
  2. Можно ли восстановить или пересоздать общее состояние?
  3. Можно ли объяснить внешние побочные эффекты?
  4. Можно ли доверять работоспособности памяти / потоков / границы с нативным кодом?

Если вы уверены во всех четырёх пунктах — можно продолжать работу. Если уверенности нет — стоит склоняться к завершению.

Особенно для долго работающих приложений, приложений мониторинга, служб и интеграции с оборудованием нередко бывают ситуации, когда жить в повреждённом состоянии опаснее, чем честно упасть.

Обработка исключений — это не «искусство никогда не падать». Это проектирование, при котором масштаб повреждений минимален, приложение честно останавливается при поломке, а восстановление проходит легко.

11. Справочные материалы

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Технические консультации и ревью дизайна

Эта тема касается упорядочивания политики обработки исключений, границ отказов, стратегии перезапуска и критериев принятия решения о продолжении работы, поэтому она хорошо сочетается с технической консультацией и ревью архитектуры.

Расследование ошибок и причин

Разделение того, стоит ли продолжать работу или завершать её после неожиданного исключения, с учётом повреждения состояния и внешних побочных эффектов, удобно вести как расследование ошибок и анализ первопричин.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Можно ли перехватывать неожиданные исключения через catch (Exception) и просто продолжать работу?
Продолжать работу, лишь записав ошибку в лог, в большинстве случаев опасно: это скрывает причину и позволяет повреждённому состоянию жить дальше. Продолжать можно только тогда, когда одновременно выполняются три условия: неудавшуюся единицу работы можно отбросить, общее состояние можно восстановить, а внешние побочные эффекты можно объяснить. Критерий решения — не «можно ли перехватить это исключение», а «можно ли после этого по-прежнему доверять состоянию приложения».
При каких исключениях нужно немедленно завершать работу?
Исключения, которые ставят под сомнение работоспособность всего процесса — StackOverflowException, AccessViolationException, серьёзный OutOfMemoryException, — безопаснее не рассматривать как повод для продолжения работы. При StackOverflowException стек вызовов уже разрушен, а AccessViolationException означает недопустимый доступ к защищённой памяти, что указывает на возможное повреждение памяти. Аномалии, возникающие на границе COM, P/Invoke или в callback-функциях сторонних SDK, также склоняют к завершению работы, причём достаточно решительному, поскольку оценить безопасность, глядя только на managed-код, сложно.
При каких условиях можно продолжать работу приложения после исключения?
Предпосылкой служит одновременное выполнение примерно таких условий: единица неудачи чётко определена (понятно, что можно отбросить — одну операцию, один экран, одно задание, одно соединение); состояние можно отбросить и пересоздать; загрязнение не распространяется на общее состояние; внешние побочные эффекты можно объяснить; пользователю можно честно сообщить, что «данная операция завершилась неудачей»; по логам и метрикам возможно последующее расследование. Если граница обработки чёткая, как в случае с одной операцией UI или одним заданием импорта, продолжение работы иногда возможно. И наоборот, аномалии в процессе обновления общего состояния, в родительском цикле, при запуске приложения или на границе с нативным кодом склоняют к завершению работы.
Можно ли безопасно продолжить работу приложения WPF, установив Handled = true в DispatcherUnhandledException?
Продолжить работу после необработанного исключения технически можно, но возможность продолжить и безопасность продолжения — разные вопросы. Обработчики вроде AppDomain.UnhandledException или DispatcherUnhandledException полезны как место последней записи информации, но не являются волшебной точкой восстановления. В итоге стабильнее подготовить сведения об исключении, контекст операции и механизм сбора дампов, чтобы можно было расследовать проблему уже после падения приложения. Особенно для долго работающих служб и приложений мониторинга падение с последующим перезапуском зачастую безопаснее и проще диагностировать, чем продолжение работы в полуразрушенном состоянии.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог