Таблица решений: завершать приложение или продолжать после неожиданного исключения
· Обновлено: · Го Комура · разработка под Windows, обработка исключений, проектирование, C# / .NET, надёжность
История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619710)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Таблица решений: завершать приложение или продолжать после неожиданного исключения. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619710 https://comcomponent.com/ru/blog/2026/03/16/005-unexpected-exception-exit-or-continue-decision-table/
- DOI (последняя версия)
- 10.5281/zenodo.21619710
- DOI (эта версия)
- 10.5281/zenodo.21619711
Скачать Excel-чеклист с листами на японском и английском
Файл состоит из двух листов, Checklist-ja и Checklist-en, и раскладывает оси этой статьи на 27 пунктов в 4 категориях: условия продолжения / условия завершения / подход к реализации / порядок решения. Столбцы Status и Notes оставлены пустыми, поэтому таблицу можно сразу использовать как журнал разбора инцидента или чеклист на ревью проекта.
Когда речь заходит о неожиданном исключении, легко скатиться к двум вариантам: «уронить или поймать и продолжить». На практике такая постановка грубовата.
Смотреть нужно на то, можно ли удержать возможную порчу в известных границах.
- Можно ли закончить неудачей только эту операцию?
- Достаточно ли заново инициализировать только этот экран / соединение / worker?
- Или уже подозрительна целостность всего процесса?
Если смотреть в таком порядке, картина обычно быстро собирается.
flowchart TB
accTitle: Порядок, в котором проверяют область порчи
accDescr: После неожиданного исключения область возможной порчи проверяют в порядке: можно ли закончить неудачей только эту операцию, достаточно ли заново инициализировать только подсистему, или подозрительна целостность всего процесса.
q1["Можно ли закончить неудачей только эту операцию?"] --> q2["Только экран или соединение заново инициализировать?"]
q2 --> q3["Подозрителен ли весь процесс?"]
Рис. 1: Возможную область порчи проверяют в порядке операция, подсистема, весь процесс.
Ниже, ориентируясь на Windows-приложения на C# / .NET, постоянно живущие приложения, службы Windows и инструменты связи с оборудованием, сводим в таблицу решений условия, при которых после неожиданного исключения можно продолжать, и условия, при которых лучше завершить работу.
1. Сначала вывод
- Глотать всё подряд через
catch (Exception)и продолжать в большинстве случаев опасно. - Продолжать можно, когда одновременно выполняются три условия: сбойную единицу можно отбросить, общее состояние можно вернуть, внешние побочные эффекты можно объяснить.
- Если граница обработки чёткая — одна операция UI, одна входная запись, одно задание — продолжение иногда возможно.
- Наоборот, если в деле общее изменяемое состояние, постоянно живущий цикл, главный поток, код запуска, native-граница или пахнет порчей памяти, склоняйтесь к завершению.
- Исключения вроде
StackOverflowException,AccessViolationException,OutOfMemoryException, которые ставят под сомнение здоровье всего процесса, безопаснее не рассматривать как повод продолжать. - В WPF и Windows Forms есть путь поймать необработанное исключение и внешне продолжить, но можно продолжить и безопасно продолжать — разные вещи.
- У долго живущих служб и приложений мониторинга чаще безопаснее и проще диагностировать падение с перезапуском, чем жизнь в наполовину сломанном состоянии.
Коротко: ось решения — можно ли восстановить инварианты.
flowchart TB
accTitle: Три условия, при которых можно продолжать
accDescr: Продолжать можно только когда одновременно можно отбросить сбойную единицу, вернуть общее состояние и объяснить внешние побочные эффекты; ось решения — можно ли восстановить инварианты.
c1["Сбойную единицу можно отбросить"] --> ok3["если все три есть — можно продолжать"]
c2["Общее состояние можно вернуть"] --> ok3
c3["Внешние побочные эффекты можно объяснить"] --> ok3
ok3 -.-> jiku["ось — можно ли восстановить инварианты"]
Рис. 2: Продолжать можно только когда одновременно выполняются три условия: единица сбоя, общее состояние, внешние побочные эффекты.
1.1 Слова, которыми пользуется эта статья
Перед таблицей решений заранее фиксируем пять слов, которые будут повторяться.
| Термин | Смысл в этой статье |
|---|---|
| Инвариант | Обещание о состоянии, которое должно быть верно до и после обработки. Например: «сумма строк совпадает с итогом», «кэш и содержимое БД согласованы», «открытое соединение обязательно есть в управляемом списке». Если это уже сломано, а процесс продолжает жить, все следующие шаги становятся подозрительными |
| Единица сбоя | Область, которую при сбое можно отбросить целиком. Одна операция, один экран, одно задание, одно соединение |
| Внешний побочный эффект | Изменение, которое уже ушло за пределы процесса. Обновление БД, запись файла, отправка письма, команда устройству — то, что catch не откатит |
| Подсистема | Единица, которую можно остановить и заново инициализировать вместе. Соединение, экран, worker, дочерний процесс |
FailFast |
Имеется в виду Environment.FailFast. API, который сразу завершает процесс, не выполняя ни try / finally, ни finalizer; подробно — в 9.7 |
Карта знаний этой статьи
Статья разбирает, что реакция на непредвиденное исключение — не проглотить его в catch (Exception) и продолжить, а решать, продолжать или завершать, по тому, можно ли объяснить инвариант разделяемого состояния, единицу отказа и внешний побочный эффект. Исключения вроде StackOverflowException, AccessViolationException и серьёзного OutOfMemoryException, сопровождаемые признаками повреждения памяти, а также сбои на нативной границе вроде COM и P/Invoke, склоняют к завершению; средствами завершения выступают Environment.FailFast и Environment.Exit. Если родительский цикл BackgroundService упал из-за непредвиденного исключения, пока процесс работает как служба Windows, без кода выхода через Environment.Exit не сработают действия восстановления службы; обработчики необработанных исключений вроде AppDomain.UnhandledException следует использовать для записи, а не как способ восстановить состояние.
flowchart LR
accTitle: Карта знаний: продолжать или завершать процесс после непредвиденного исключения
accDescr: Схема, которая показывает, что решение продолжать или завершать после непредвиденного исключения опирается на проверку инвариантов, единицы отказа и внешнего побочного эффекта; что исключения с признаками повреждения памяти и сбои на нативной границе склоняют к завершению; что при непредвиденном исключении в BackgroundService код выхода через Environment.Exit нужен для действий восстановления службы Windows; и что обработчики необработанных исключений служат для записи, а не для восстановления
unexpected_exception["непредвиденное исключение"]
invariant["инвариант"]
continue_processing["продолжение обработки"]
failure_unit["единица сбоя"]
external_side_effect["внешний побочный эффект"]
catch_exception_antipattern["глотание исключений в catch (Exception)"]
zombie_process["зомбирование процесса"]
terminate_process["завершение процесса"]
stackoverflowexception["StackOverflowException"]
accessviolationexception["AccessViolationException"]
environment_failfast["Environment.FailFast"]
outofmemoryexception["OutOfMemoryException"]
memory_corruption_symptom["признаки повреждения памяти"]
environment_exit["Environment.Exit"]
native_interop_boundary["нативная граница (COM/P/Invoke)"]
backgroundservice["BackgroundService"]
stopapplication["IHostApplicationLifetime.StopApplication"]
windows_service["служба Windows"]
unhandled_exception_handler["обработчик необработанных исключений"]
exception_recovery_attempt["попытка восстановления после исключения"]
continue_processing -->|"требует"| invariant
continue_processing -->|"требует"| failure_unit
continue_processing -->|"требует"| external_side_effect
catch_exception_antipattern -.->|"может вызвать"| zombie_process
catch_exception_antipattern -->|"не рекомендуется"| unexpected_exception
terminate_process -->|"рекомендуется для"| stackoverflowexception
terminate_process -->|"рекомендуется для"| accessviolationexception
environment_failfast -->|"рекомендуется для"| outofmemoryexception
accessviolationexception -.->|"может вызвать"| memory_corruption_symptom
environment_failfast -->|"рекомендуется для"| memory_corruption_symptom
environment_failfast -->|"реализует"| terminate_process
environment_exit -->|"реализует"| terminate_process
native_interop_boundary -.->|"может вызвать"| memory_corruption_symptom
terminate_process -.->|"рекомендуется для"| native_interop_boundary
backgroundservice -.->|"требует"| environment_exit
stopapplication -.->|"не рекомендуется"| windows_service
backgroundservice -->|"реализует"| windows_service
unhandled_exception_handler -->|"не рекомендуется"| exception_recovery_attempt
catch_exception_antipattern -.->|"несовместимо с"| backgroundservice
environment_failfast -.->|"предотвращает"| external_side_effect
unexpected_exception -->|"проверяется"| unhandled_exception_handler
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 21, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Что в этой статье называется «неожиданным исключением»
2.1 Отделяем ожидаемое от неожиданного
Сначала: редкое исключение и неожиданное исключение — не одно и то же.
Например, следующее даже при низкой частоте можно считать ожидаемым.
- Пользователь выбрал несуществующий файл
- Удалённая сторона временно не ответила по тайм-ауту
- Одна строка импортируемого CSV была битой
- При отмене операции вылетело
OperationCanceledException - Нарушение бизнес-правила, из-за которого нужно провалить только эту операцию
Это тот тип сбоев, для которых обработку можно заранее решить на этапе проектирования.
С другой стороны, неожиданные исключения, о которых в основном эта статья, выглядят так.
- Сломалось предположение в собственном коде, и вылетело
NullReferenceExceptionилиInvalidOperationException - Исключение вылетело посреди обновления общего состояния, и непонятно, насколько далеко оно успело примениться
- Упал родительский цикл мониторинга или обработки сообщений
- Аномалия на границе COM / P/Invoke / vendor SDK
- Как у
AccessViolationExceptionилиStackOverflowException, «медосмотр» процесса уже красный
Иначе говоря, это случаи, когда «после этого исключения непонятно, можно ли ещё доверять состоянию приложения».
flowchart TB
accTitle: Где проходит граница ожидаемого и неожиданного
accDescr: Если обработку сбоя можно заранее решить на этапе проектирования, исключение даже при низкой частоте можно считать ожидаемым; неожиданное в этой статье — то, после чего непонятно, можно ли ещё доверять состоянию приложения.
e1["Исключение произошло"] --> q1{"Обработку можно заранее решить на этапе проектирования?"}
q1 -->|"можно"| soutei["ожидаемое (частота может быть низкой)"]
q1 -->|"нельзя"| sotogai["неожиданное (неясно, можно ли доверять состоянию)"]
Рис. 3: Редкое исключение и неожиданное исключение — разные вещи; граница в том, можно ли заранее решить обработку на этапе проектирования.
2.2 Кажется, что выбор из двух, на деле их три
Разговор путает то, что «продолжение» рассматривают как один вариант.
На практике обычно три уровня.
| Выбор | Смысл |
|---|---|
| Провалить только эту операцию и продолжить | Экран остаётся, но именно это сохранение или импорт считается неудавшимся |
| Остановить только подсистему и продолжить | Заново инициализировать только соединение, экран, worker или дочерний процесс |
| Завершить процесс | Область порчи состояния не читается, поэтому исходят из перезапуска |
Фраза «приложение продолжает работу» может значить и идти дальше так, будто ничего не было, и продолжить, отрезав сломанную часть — вес разный.
flowchart TB
accTitle: Одно и то же «продолжение», разный вес
accDescr: «Приложение продолжает работу» может значить и идти дальше так, будто ничего не было, и продолжить, отрезав сломанную часть — это вещи разного веса.
keizoku["Приложение продолжает работу"] --> k1["идти дальше так, будто ничего не было"]
keizoku --> k2["продолжить, отрезав сломанную часть"]
k1 -.-> omomi["одно «продолжение», разный вес"]
k2 -.-> omomi
Рис. 4: Не считать продолжение одним видом; отделять продолжение, которое отрезает сломанную часть.
3. Таблица решений, на которую смотреть сначала
3.1 Общая картина
Если начать с этой таблицы, общее направление обычно видно.
| Ситуация | Первый выбор | Почему |
|---|---|---|
| Неудачей закончились только один ввод, одна операция на экране или одно задание, и их состояние можно отбросить | Склоняться к продолжению | Единицу сбоя можно удержать в границах |
| После исключения затронутый объект или соединение можно уничтожить и собрать заново | Склоняться к повторной инициализации подсистемы | Область порчи можно локализовать |
| Общее состояние обновлено наполовину, и непонятно, насколько далеко это применилось | Склоняться к завершению | Возможно, инвариант уже сломан |
| Внешние побочные эффекты — БД / файлы / команды устройству — выполнены наполовину, и нельзя объяснить дублирование или потерю | Склоняться к завершению | Согласованность с внешним миром не читается |
| Цикл мониторинга, цикл переподключения или родительский цикл обработки сообщений упал из-за неожиданного исключения | Склоняться к завершению | Если часть функций тихо умрёт, легко получить зомби-процесс |
| Сбой в коде запуска, чтении настроек, композиции DI или инициализации обязательной зависимости | Склоняться к завершению как к сбою запуска | Частичный запуск опаснее |
AccessViolationException, StackOverflowException, тяжёлый OutOfMemoryException, пахнет порчей на native-стороне |
Склоняться к немедленному завершению | Под вопросом здоровье всего процесса |
| Опасная обработка изолирована в отдельном процессе, родительский процесс не задет | Родитель продолжает, дочерний перезапускают | Область отказа уже отделена |
flowchart TD
accTitle: Поток решения по таблице
accDescr: Решение идёт в порядке: пахнет ли порчей памяти / исчерпанием стека / критическим исчерпанием ресурсов, можно ли отбросить сбойную единицу, можно ли откатить или заново инициализировать общее состояние, можно ли объяснить внешние побочные эффекты.
A["Неожиданное исключение"] --> B{"Пахнет порчей памяти / исчерпанием стека / критическим исчерпанием ресурсов?"}
B -- "да" --> Z["Завершение / FailFast / перезапуск"]
B -- "нет" --> C{"Сбойную единицу можно отбросить?"}
C -- "нет" --> Y["Склоняться к завершению"]
C -- "да" --> D{"Общее состояние можно откатить / заново инициализировать?"}
D -- "нет" --> X["Остановить подсистему или завершить"]
D -- "да" --> E{"Внешние побочные эффекты можно объяснить?"}
E -- "нет" --> X
E -- "да" --> W["Продолжить, считая неудачной только эту операцию"]
Рис. 5: Направление выбирают, проверяя по очереди запах порчи, единицу сбоя, общее состояние и внешние побочные эффекты.
«Запах порчи памяти» на первой развилке подробно разбирается в 3.4, FailFast справа вверху — в 9.7.
3.2 Что смотреть раньше типа исключения
Не стоит решать сразу по одному типу исключения. Сначала лучше проверить вот это.
| На что смотреть | Что проверять |
|---|---|
| Где произошло | Событие UI, одно задание, родительский цикл, код запуска, native-граница |
| Насколько далеко успело пройти | Не изменились ли по пути состояние в памяти, БД, файлы, состояние устройства |
| Возможная область порчи | Только этот объект, весь экран или весь процесс |
| Возможен ли откат | Можно ли уничтожить и собрать заново, откатить транзакцией |
| Внешние побочные эффекты | Отправлено или нет, безопасно ли повторное выполнение, возможна ли компенсация |
| Наблюдение / перезапуск | Есть ли автоматический перезапуск или путь восстановления после падения |
3.3 Особо опасные исключения
Разбирать все типы по отдельности не нужно, но есть такие, которые не стоит рассматривать как повод продолжать.
| Исключение / признак | Первый выбор | Почему смотреть |
|---|---|---|
StackOverflowException |
Склоняться к немедленному завершению | Стек вызовов уже сломан, обычное восстановление предполагать трудно |
AccessViolationException |
Склоняться к немедленному завершению | Недопустимый доступ к защищённой памяти; подозревают native-границу или порчу памяти |
OutOfMemoryException |
Склоняться к завершению | Код восстановления, которому самому нужны новые выделения, легко становится нестабильным |
Неожиданный NullReferenceException / InvalidOperationException |
Зависит от контекста, но склоняться к завершению | Сломалось собственное предположение; по пути могли остаться незавершённые изменения |
| Неожиданное исключение, вытекшее из родительского цикла | Склоняться к завершению | Ядро функции уже мертво, а процесс рискует остаться живым |
| Аномалии, которые стартуют с callback COM / P/Invoke / vendor SDK | От немедленного до решительного завершения | По одному managed-коду безопасность оценить трудно |
3.4 Откуда берутся слова «пахнет порчей памяти» и «зомби»
Эти два выражения звучат как ощущения, но на деле смотрят на конкретные вещи.
Сначала симптомы, по которым подозревают порчу памяти.
| Что видно | Где смотреть |
|---|---|
Процесс падает с кодом исключения 0xc0000005 (нарушение доступа) или 0xc0000374 (порча кучи) |
Просмотр событий > Журналы Windows > Приложение, «Ошибка приложения» (событие 1000) |
| Место падения каждый раз другое. Падает в коде, который непосредственно перед этим не трогали | Логи, стек вызовов в дампе |
| Падает только когда трогают объект или handle, который уже должны были освободить | Шаги воспроизведения, дамп |
Падает в native-освобождении (free / delete / освобождение COM) |
Стек вызовов в дампе (падение внутри ntdll.dll и т. п.) |
| Значения, которые не трогали, переписываются. При том же входе результат другой | Логи входа/выхода, сравнение повторных прогонов |
0xc0000005 — STATUS_ACCESS_VIOLATION, 0xc0000374 — STATUS_HEAP_CORRUPTION; оба значения есть в списке NTSTATUS у Microsoft. Особенно порча кучи проявляется не в момент поломки, а у того, кто следующим тронул уже сломанную кучу, поэтому место падения не обязательно виновник. Если симптомы такого вида есть, безопаснее считать, что ловить на managed-стороне и продолжать бессмысленно.
flowchart TB
accTitle: Сдвиг места падения при порче кучи
accDescr: При порче кучи падение происходит не в момент поломки, а у того, кто следующим тронул уже сломанную кучу, поэтому место падения не обязательно виновник.
h1["Где-то ломается куча"] --> h2["в этот момент ещё не падаем"]
h2 --> h3["падает тот, кто тронул следующим"]
h3 -.-> h4["место падения не обязательно виновник"]
Рис. 6: При порче кучи падает тот, кто тронул следующим, поэтому место падения и место поломки расходятся.
Дальше симптомы, по которым подозревают зомби.
| Что видно | Где смотреть |
|---|---|
| Процесс жив, но время последней обработки не обновляется | Лог времени последней обработки, heartbeat |
| Растёт только число застрявших элементов в очереди или во входной папке | Длина очереди, число необработанных файлов |
| С какого-то момента логи внезапно обрываются | Логи приложения |
| Экран ещё слушается, а обновление сзади уже стоит | Сверка того, что на экране, с реальными данными |
| Число рабочих потоков меньше ожидаемого | Диагностический лог, список потоков в Process Explorer |
Зомби — это не «ещё не упал», а жив, но работу не делает. Если заранее завести показатели «я ещё работаю» — время последней обработки, число застрявших элементов, heartbeat — и решение продолжать или завершать, и последующий разбор становятся заметно проще.
flowchart TB
accTitle: Показатели, которыми ловят зомби
accDescr: Зомби — это жить, не делая работу; если заранее завести показатели вроде времени последней обработки, числа застрявших элементов и heartbeat, и решение, и последующий разбор становятся проще.
z1["Время последней обработки"] --> z4["сначала завести показатели «я работаю»"]
z2["Число застрявших элементов"] --> z4
z3["heartbeat"] --> z4
z4 --> z5["проще решить, продолжать или завершать"]
z4 --> z6["проще разбирать потом"]
Рис. 7: Зомби ловят не по тому, что процесс ещё не упал, а по показателям, что работа идёт.
4. Решение в зависимости от того, где это произошло
4.1 События UI
У событий UI — клик по кнопке, переход между экранами, поиск, выбор файла — сравнительно много пространства для продолжения. Но есть условия.
Продолжать проще, когда:
- сбой случился до загрузки, и бизнес-состояние ещё не трогали;
- сломано только временное состояние внутри диалога, и закрытие диалога его отбрасывает;
- после исключения можно заново собрать ViewModel или соединение;
- пользователю можно честно сказать: «эта операция не удалась».
Наоборот, к завершению склоняет, когда:
- наполовину обновлены и экран, и доменное состояние;
- тронули общее состояние, которое видят другие экраны — static / singleton / кэш;
- после исключения остались активность кнопки или состояние выбора, и согласованность уже не читается;
- на UI-потоке вылетело неожиданное исключение, и непонятно, насколько далеко прошли отрисовка или уведомления.
flowchart TB
accTitle: Развилка исключения в операции UI
accDescr: Исключение в событии UI легче продолжить, если затронуто только временное состояние, которое можно отбросить закрытием; если тронуто общее состояние или обновление наполовину, склоняются к завершению.
u1["Неожиданное исключение в операции UI"] --> q1{"Какое состояние тронули?"}
q1 -->|"только временное, которое можно отбросить"| u2["продолжить, провалив только эту операцию"]
q1 -->|"общее состояние или обновление наполовину"| u3["склоняться к завершению"]
Рис. 8: У события UI пространства для продолжения много, но если тронуто общее состояние, расклад другой.
4.2 Задания / запросы, которые обрабатывают по одному
Здесь граница, где продолжение даётся легко.
- Одно сообщение
- Один файл
- Один HTTP-запрос
- Одно задание импорта
- Один элемент пакетной обработки
Если такие единицы ясны, можно провалить только этот один элемент и идти дальше.
Но есть предпосылки.
- Единица сбоя ясна снаружи
- Промежуточные изменения приводятся в порядок транзакцией или компенсацией
- Повторный прогон той же обработки не портит результат
- Сбой можно увести в карантинную очередь или журнал ошибок
4.3 Постоянно живущие циклы / мониторинг / обработка очереди
Здесь неаккуратное продолжение опаснее всего.
Например:
- циклы переподключения;
- циклы мониторинга;
- циклы потребления очереди;
- периодический опрос;
- мониторинг состояния устройства;
- постоянно живущая обработка в приложении в области уведомлений (tray).
Страшный сценарий здесь — когда родительский цикл умирает от одного неожиданного исключения, а процесс остаётся жить сам по себе.
Здесь политику лучше разделить.
- На границе обработки каждого элемента ловить ожидаемые исключения
- Если неожиданное исключение вытекло из родительского цикла, склоняться к завершению процесса
flowchart TB
accTitle: Как разделять политику в постоянно живущем цикле
accDescr: На границе обработки каждого элемента ловят ожидаемые исключения; если неожиданное вытекло из родительского цикла, склоняются к завершению процесса.
loop1["Постоянно живущий цикл"] --> b1["граница обработки каждого элемента"]
loop1 --> b2["родительский цикл"]
b1 --> r1["ловить ожидаемые исключения"]
b2 --> r2["если вытекло неожиданное — склоняться к завершению"]
r2 -.-> zb["не оставлять в живых один процесс"]
Рис. 9: Политику на границе элемента и в родительском цикле разделяют; неожиданное исключение из родительского цикла ведут к завершению.
4.4 Код запуска
Если к сбою при запуске относиться как к «сначала как-нибудь поднимемся, разберёмся потом», процесс продолжает жить с дырой в функциях, и потом причину отделить уже труднее.
- не читаются обязательные настройки;
- не удалась миграция версии;
- нет обязательной папки или сертификата;
- не удалась инициализация ключевой службы;
- сломана конфигурация зависимостей.
В таких случаях понятнее завершить как сбой запуска.
4.5 Native-граница / COM / P/Invoke / unsafe
Здесь смотреть отдельно и чуть строже.
- COM
- P/Invoke
- код за пределами C++/CLI
- vendor SDK
- native-код, который возвращается через callback
- обработка с
unsafe
Особенно к завершению склоняет, если видно следующее.
AccessViolationException- симптомы порчи кучи или double free
- аномалии handle, запах use-after-free
- внезапная смерть на границе callback
flowchart TB
accTitle: Как обращаться с аномалией на native-границе
accDescr: На native-границе вроде COM и P/Invoke, если видны AccessViolationException, симптомы порчи кучи или внезапная смерть на границе callback, склоняются к завершению; саму границу смотрят отдельно и строже.
n4["AccessViolationException"] --> n3["склоняться к завершению"]
n5["симптомы порчи кучи или double free"] --> n3
n6["внезапная смерть на границе callback"] --> n3
n3 -.-> n2["native-границу смотрят отдельно и строже"]
Рис. 10: Если на native-границе видны такие симптомы, предпосылку «продолжим» отбрасывают и склоняются к завершению.
5. Условия, при которых можно продолжать
Условия, при которых продолжение допустимо, сводятся к следующему. Предпосылка — что они выполняются примерно все сразу.
| Условие | Смысл |
|---|---|
| Единица сбоя ясна | Понятно, что отбросить: одну операцию, один экран, одно задание, одно соединение |
| Состояние можно отбросить | Можно уничтожить и собрать заново или считать неприменённым |
| Общее состояние защищено | Заражение не расползается на другие функции |
| Внешние побочные эффекты можно объяснить | Понятно: отправили / не отправили / можно ли отправить снова |
| Можно быть честным с пользователем | Можно показать «эта операция не удалась» |
| Есть наблюдаемость | По логам, метрикам и дампам можно потом разобрать, что случилось |
6. Условия, при которых лучше завершить
Наоборот, если подходит что-то из следующего, склоняйтесь к завершению.
- непонятно, что именно успели изменить по пути;
- тронуто общее изменяемое состояние, и согласованность не читается;
- сломано управление временем жизни блокировок, очередей, потоков или циклов мониторинга;
- нельзя объяснить дублирование, потерю или незавершённость внешних побочных эффектов;
- не удался запуск или инициализация ключевой инфраструктуры;
- подозревают native-границу или порчу памяти.
На этом уровне усилия красиво продолжить уступают усилиям упасть так, чтобы потом легко подняться.
flowchart TB
accTitle: Какие усилия работают, когда склоняются к завершению
accDescr: Если уже выполняются условия в пользу завершения, усилия красиво продолжить работают слабо; лучше вкладываться в то, чтобы упасть и легко восстановиться.
j1["Подходят условия в пользу завершения"] --> j2["усилия красиво продолжить"]
j1 --> j3["усилия упасть так, чтобы легко восстановиться"]
j2 -.-> j4["на этом уровне работают слабо"]
j3 -.-> j5["работают лучше"]
Рис. 11: Когда ситуация уже в пользу завершения, вкладываться стоит не в продолжение, а в лёгкость восстановления.
7. Рекомендации по типичным сценариям
Таблица в 3.1 написана в виде условий, поэтому при примерке к реальной ситуации можно застрять. Эта таблица — те же условия, приложенные к часто встречающимся сценам. Если застряли, быстрее сначала проверить условия в 3.1, затем в этой таблице искать близкую строку.
flowchart TB
accTitle: Как пользоваться таблицами, если застряли
accDescr: Если примерка к сцене буксует, сначала проверяют условия в таблице решений 3.1, затем в таблице этой главы ищут близкую строку.
m1["Застряли на примерке к сцене"] --> m2["проверить условия в таблице 3.1"]
m2 --> m3["искать близкую строку в таблице этой главы"]
Рис. 12: От таблицы условий к таблице сцен — в таком порядке меньше путаницы.
| Сценарий | Рекомендация | Почему |
|---|---|---|
| В кнопке открытия файла указан несуществующий путь | Продолжить, провалив только эту операцию | Порча состояния локальна |
| В импорте CSV бита только одна строка | Продолжить, провалив одну строку или один файл | Единицу сбоя легко удержать в границах |
Посреди сохранения экрана вылетело неожиданное NullReferenceException |
От пересоздания экрана до завершения | Непонятно, насколько далеко изменились ViewModel / бизнес-состояние |
| Одно сообщение из очереди нарушило бизнес-правило | Продолжить, провалив только это сообщение | Его можно увести в карантинную очередь |
| Родительский цикл потребления очереди упал из-за неожиданного исключения | Склоняться к завершению процесса | Сломано время жизни всего worker |
| При запуске не читаются обязательные настройки | Завершить как сбой запуска | Частичный запуск опаснее |
AccessViolationException вокруг callback vendor SDK |
Склоняться к немедленному завершению | Возможность порчи памяти нельзя игнорировать |
| Не удалась только несущественная отправка телеметрии | Выключить только эту функцию и продолжить | Область отказа отделяется от основной функции |
8. Типичные ошибки
8.1 catch (Exception), запись в лог и продолжение
Это довольно опасно. Причина прячется, а сломанное состояние легко продолжает жить.
8.2 Попытка восстановиться в последнем обработчике необработанных исключений
AppDomain.UnhandledException, Application.ThreadException, DispatcherUnhandledException и подобные полезны как место последней записи, но не являются волшебной точкой восстановления.
8.3 Беспечный retry при внешних побочных эффектах
Если без гарантии безопасного повторного выполнения повторять команды устройству, отправку писем, списание, перемещение файлов или обновление БД, на первый план выходит уже инцидент двойного выполнения.
flowchart TB
accTitle: Беспечный retry приводит к двойному выполнению
accDescr: Если обработку с внешним побочным эффектом повторяют без гарантии безопасного повторного выполнения, на первый план выходит инцидент двойного выполнения.
y1["Сбой обработки с внешним побочным эффектом"] --> y2["retry без безопасного повторного выполнения"]
y2 --> y3["на первый план выходит инцидент двойного выполнения"]
y1 -.-> y4["команды устройству, списание, отправка и т. п."]
Рис. 13: Retry без безопасного повторного выполнения приводит к инциденту двойного выполнения.
8.4 Цикл мониторинга уже мёртв, а UI ещё жив
Приложение, которое с виду живое, а работу не делает, с точки зрения пользователя выглядит нормальным, поэтому обнаружение запаздывает. Стоит сделать так, чтобы сторона наблюдения ловила симптомы зомби из 3.4.
8.5 Говорить «не хотим падать», не спроектировав падение
Если падать не хотите, сначала нужно положить вот это.
- автоматический перезапуск
- восстановление сеанса
- сохранение промежуточных результатов
- безопасное повторное выполнение
- отделение области отказа
9. На что смотреть при реализации
9.1 Сдвигать места catch к границам
Вместо того чтобы ловить всё подряд в глубоких слоях, проще навести порядок, если принимать исключения там, где можно определить единицу сбоя:
- граница операции UI;
- граница одного запроса;
- граница одного задания;
- граница одного соединения;
- граница процесса.
Например, в импорте по одному элементу catch ставят внутри цикла. Это и есть единица сбоя.
// C# / .NET 8. Пример цикла по одному элементу: единица сбоя замкнута внутри цикла.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
public sealed record ImportItem(string Id, string Payload);
public sealed record ImportFailure(string Id, string ExceptionType, string Message);
public sealed record ImportSummary(int Succeeded, IReadOnlyList<ImportFailure> Failed);
public interface IImportStore
{
/// <summary>
/// Сбой одного элемента (ошибка проверки, дубликат, битый формат и т. п.) бросать как ImportItemException.
/// Всё остальное — не «проблема этого одного элемента», поэтому выпускать наружу как есть.
/// </summary>
Task SaveAsync(ImportItem item, CancellationToken cancellationToken);
}
/// <summary>Сбой одного элемента, который можно отбросить и идти дальше.</summary>
public sealed class ImportItemException(string message, Exception? inner = null)
: Exception(message, inner);
public sealed class ImportRunner(IImportStore store, ILogger<ImportRunner> logger)
{
public async Task<ImportSummary> RunAsync(
IReadOnlyList<ImportItem> items,
CancellationToken cancellationToken)
{
int succeeded = 0;
List<ImportFailure> failed = [];
foreach (ImportItem item in items)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
await store.SaveAsync(item, cancellationToken);
succeeded++;
}
catch (ImportItemException ex)
{
// Состояние одного элемента можно отбросить, поэтому записываем и идём к следующему.
//
// Не делать здесь catch (Exception). Если проглотить и NullReferenceException, и
// OutOfMemoryException как «просто плохие данные», неожиданное исключение, которое
// в 4.3 решили останавливать вместе с хостом, до родительского цикла не дойдёт.
// После того как состоянию уже нельзя доверять, вы продолжите писать остальные элементы.
logger.LogError(ex, "Import failed. ItemId={ItemId}", item.Id);
failed.Add(new ImportFailure(item.Id, ex.GetType().Name, ex.Message));
}
}
return new ImportSummary(succeeded, failed);
}
}
Обратная сторона того же решения: на стороне родительского цикла, который крутит эту обработку, не глотать. Как в 4.3, хуже всего, когда родительский цикл уже мёртв, а процесс ещё жив, поэтому неожиданное исключение ведут к остановке хоста.
// Сторона постоянно живущего цикла. Запрос остановки выходит как нормальный путь;
// любое другое неожиданное исключение останавливает хост целиком.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public interface IImportQueue
{
Task<IReadOnlyList<ImportItem>> DequeueBatchAsync(CancellationToken cancellationToken);
}
public sealed class ImportWorker(
IImportQueue queue,
ImportRunner runner,
IHostApplicationLifetime lifetime,
ILogger<ImportWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
using PeriodicTimer timer = new(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
IReadOnlyList<ImportItem> batch = await queue.DequeueBatchAsync(stoppingToken);
ImportSummary summary = await runner.RunAsync(batch, stoppingToken);
logger.LogInformation(
"Batch finished. Succeeded={Succeeded} Failed={Failed}",
summary.Succeeded,
summary.Failed.Count);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Запрос остановки — нормальный путь. Здесь тихо выходим.
}
catch (Exception ex)
{
// Сломался родительский цикл = сломано время жизни всего worker.
// Не глотать и не оставлять «жив только процесс»: останавливаем и отдаём на перезапуск.
logger.LogCritical(ex, "Worker loop failed unexpectedly. Stopping the host.");
// StopApplication — это запрос «аккуратно сложиться». Одного этого мало:
// сторона наблюдения, которая перезапускает только при сбое (восстановление службы,
// Restart=on-failure у systemd, политика restart контейнера), не отличит это
// от «работу закончили нормально», и процесс больше не встанет.
//
// Поставить Environment.ExitCode тоже недостаточно. Если крутимся как служба Windows,
// при нормальной остановке хоста SCM получает SERVICE_STOPPED, ExitCode в состояние
// завершения службы не попадает, и операции восстановления не стартуют.
// Процесс завершают с ненулевым кодом.
Environment.Exit(1);
}
}
}
Ключевое место — вызов Environment.Exit(1). IHostApplicationLifetime.StopApplication() — это запрос нормальной остановки, поэтому при работе как службы Windows SCM получает SERVICE_STOPPED. Environment.ExitCode процесса в состояние завершения службы не попадает, поэтому операции восстановления, заданные в свойствах службы («перезапустить службу»), не стартуют. В официальном руководстве по worker-службе прямо сказано: поведение по умолчанию BackgroundServiceExceptionBehavior.StopHost «аккуратно останавливает», поэтому сторона управления службами Windows не перезапускает; чтобы заработали операции восстановления, нужно вызвать Environment.Exit с ненулевым кодом (см. справочные материалы в главе 11).
Environment.Exit убивает текущий процесс, поэтому логи, которые хотите успеть отдать до падения, нужно дописать до этой строки. Если логгер буферизует, вставьте сброс. Наоборот, если напротив не служба Windows, а только systemd или контейнер, можно выставить Environment.ExitCode и сложиться через StopApplication(): сторона наблюдения всё равно увидит сбой. Выбирайте по тому, в какой среде это крутится.
Ещё одна точка: роль catch внутри цикла и снаружи разная. Внутри — запись единицы сбоя, снаружи — конец времени жизни. Если перепутать, одно сбойное задание уронит приложение, или наоборот worker уже мёртв, а процесс ещё жив.
flowchart TB
accTitle: Роли catch внутри и снаружи
accDescr: Catch внутри цикла записывает единицу сбоя, снаружи завершает время жизни; если перепутать, либо одно сбойное задание уронит приложение, либо умрёт worker, а процесс останется.
c1["catch внутри цикла"] --> c2["запись единицы сбоя"]
c3["catch снаружи цикла"] --> c4["конец времени жизни"]
c2 -.-> ng1["если перепутать — падаем от одного сбоя"]
c4 -.-> ng2["если перепутать — остаётся только процесс"]
Рис. 14: У catch внутри и снаружи цикла разные роли; путаница даёт аварию в обе стороны.
9.2 Отделять ожидаемые исключения от неожиданных
- Ожидаемые: validation, not found, timeout, cancel, нарушение бизнес-правила
- Неожиданные: сломанное предположение, утечка из родительского цикла, аномалия на native-границе, запах порчи памяти
9.3 Уменьшать общее состояние
Чем больше общее изменяемое состояние, тем труднее решение о продолжении. Наоборот, чем сильнее удаётся замкнуть его внутри одного экрана, одной сессии, одного worker, тем легче удержать и сбой.
9.4 Опасную обработку уводить в отдельный процесс
Для всего, для чего не хотите расползания ущерба при падении — COM / ActiveX / vendor SDK / unsafe / тяжёлая обработка изображений / управление внешним оборудованием — вынос в отдельный процесс заметно помогает.
9.5 Обработчики необработанных исключений — для «записи», не для «восстановления»
- сведения об исключении
- контекст операции
- важные логи непосредственно перед падением
- настройки / версия / адреса подключения
- путь снятия дампа
Если это собрать заранее и сделать приоритетом возможность разобраться уже после падения, система в итоге стабильнее.
Для WPF обработчика «только запись» хватает примерно такого вида.
// App.xaml.cs для WPF (.NET 8). Обработчики — для «записи», не для «восстановления».
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
namespace SampleApp;
public partial class App : Application
{
private static readonly string CrashLogPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SampleApp",
"crash.log");
protected override void OnStartup(StartupEventArgs e)
{
// Регистрацию обработчиков ставить до base.OnStartup(e).
// base.OnStartup поднимает событие Startup; если исключение вылетит
// у подписчика, обработчики, написанные ниже, ещё не зарегистрированы,
// и получается самый неприятный вид: «упали на запуске, в логе ни строки».
// Именно сбой самого запуска и хочется записать, поэтому вешаем заранее.
// Исключение, которое на UI-потоке дошло необработанным
DispatcherUnhandledException += (_, args) =>
{
Record("DispatcherUnhandledException", args.Exception);
// args.Handled = true позволит продолжить, но можно ли продолжать —
// смотреть по условиям главы 5. Если уверенности нет, записать
// и оставить поведение по умолчанию (завершение).
args.Handled = false;
};
// Последнее уведомление, в том числе не с UI-потока. Здесь уже не остановить, только записать.
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
Record("AppDomain.UnhandledException", args.ExceptionObject as Exception);
// Исключение Task, которое так и не await-нули и потом собрали
TaskScheduler.UnobservedTaskException += (_, args) =>
{
Record("UnobservedTaskException", args.Exception);
args.SetObserved();
};
// Когда всё повешено, идём в запуск по умолчанию (подъём события Startup)
base.OnStartup(e);
}
private static void Record(string source, Exception? exception)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(CrashLogPath)!);
string version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
string text = string.Join(
Environment.NewLine,
$"[{DateTimeOffset.Now:O}] {source}",
$"version={version} os={Environment.OSVersion} user={Environment.UserName}",
exception?.ToString() ?? "(no exception object)",
string.Empty);
File.AppendAllText(CrashLogPath, text);
}
catch
{
// Сбой записи не должен останавливать завершение.
}
}
}
Главное — внутри обработчика не чинить состояние. Здесь делают только одно: оставить материал, по которому потом можно разобрать тот же симптом.
flowchart TB
accTitle: Поток обработчика только для записи
accDescr: В WPF обработчики регистрируют до base.OnStartup, затем идут в запуск; внутри обработчика состояние не чинят, оставляют только материал, по которому потом можно разобрать тот же симптом.
w1["Сначала зарегистрировать обработчики"] --> w2["идти в base.OnStartup"]
w2 --> w3["записать необработанное исключение"]
w3 -.-> w4["состояние не чинить"]
Рис. 15: Регистрацию заканчивают до запуска; в обработчике только записывают.
9.6 Не переоценивать события необработанных исключений в WPF / WinForms
В WPF, если в DispatcherUnhandledException поставить Handled = true, после необработанного исключения действительно можно продолжить.
В Windows Forms на главном UI-потоке способ остановки тоже можно выбрать через Application.ThreadException и SetUnhandledExceptionMode.
Но то, можно ли продолжить, и то, выполнены ли условия восстановления, — разные вопросы.
9.7 Environment.FailFast — только когда «убирать опаснее»
FailFast на блок-схеме в 3.1 — это Environment.FailFast. Поведение, которое описано в официальной документации, такое.
- Процесс завершается, не выполняя ни текущие
try/finally, ни finalizer - В Windows переданное сообщение пишется в журнал событий приложений Windows, затем снимается дамп приложения, и уже потом процесс завершается
- Сообщение и сведения об исключении входят и в отчёт об ошибке в Microsoft через Windows Error Reporting
- Если вызвать под отладчиком Visual Studio, получается
ExecutionEngineExceptionи срабатывает managed debugging assistant fatalExecutionEngineError
То, что finally не выполняется, — не недостаток, а цель этого API. Если при уже сломанном состоянии гонять код уборки, сломанное содержимое иногда записывают в файл или БД. В документации тоже сказано: если состояние приложения сломано неисправимо, и выполнение try / finally или finalizer само испортит ресурсы, использовать не Environment.Exit, а FailFast.
Как выбирать:
| Ситуация | Что брать |
|---|---|
| Инвариант сломан, гонять уборку опаснее | Environment.FailFast |
| Состояние здорово, хотим сначала убраться и выйти | Нормальное завершение (IHostApplicationLifetime.StopApplication и т. п.) |
| Просто вернуть код завершения и выйти | Environment.Exit или return из Main |
В коде это место выглядит так.
// C# / .NET 8. Место, где обнаружили, что инвариант общего состояния сломан.
// Дальше ни одной уборке доверять нельзя.
if (cache.Count != store.Count)
{
Environment.FailFast(
$"Invariant broken: cache={cache.Count} store={store.Count}",
new InvalidOperationException("Cache and store are out of sync."));
}
Дамп снимается сам, поэтому в сообщение FailFast стоит класть какой инвариант какими значениями сломался — последующий разбор становится заметно проще. Наоборот, вызывать FailFast на ожидаемый сбой вроде опечатки во вводе или ошибки связи — избыточно. Это возвращает к разговору об единице сбоя в 9.1.
flowchart TB
accTitle: Почему FailFast не гоняет уборку
accDescr: Если при неисправимо сломанном состоянии гонять код уборки, можно записать сломанное содержимое, поэтому Environment.FailFast завершает процесс, не выполняя try, finally и finalizer, а в Windows оставляет журнал событий и дамп.
f1["Инвариант сломан"] --> f2["сразу завершить через FailFast"]
f2 --> f3["не выполнять ни finally, ни finalizer"]
f2 --> f4["оставить дамп и журнал событий (Windows)"]
f3 -.-> f5["не дать записать сломанное содержимое"]
Рис. 16: FailFast не гоняет уборку и тем самым не даёт записать сломанное состояние.
10. Итог
Когда случилось неожиданное исключение, смотреть нужно не на «можно ли поймать это исключение», а на то, можно ли после этого ещё доверять состоянию приложения.
Порядка решения обычно хватает такого.
- Можно ли отбросить сбойную единицу?
- Можно ли вернуть или заново собрать общее состояние?
- Можно ли объяснить внешние побочные эффекты?
- Можно ли доверять здоровью памяти / потоков / native-границы?
Если во всех четырёх уверены — можно продолжать. Если уверенности нет — склоняйтесь к завершению.
flowchart TB
accTitle: Порядок решения в итоге
accDescr: По очереди проверяют, можно ли отбросить единицу сбоя, вернуть общее состояние, объяснить внешние побочные эффекты и доверять здоровью вплоть до native-границы; продолжают только если во всех четырёх уверены, иначе склоняются к завершению.
g1["Можно ли отбросить сбойную единицу?"] --> g2["Можно ли вернуть общее состояние?"]
g2 --> g3["Можно ли объяснить внешние побочные эффекты?"]
g3 --> g4["Можно ли доверять здоровью?"]
g4 -->|"во всех четырёх уверены"| g5["можно продолжать"]
g4 -->|"уверенности нет"| g6["склоняться к завершению"]
Рис. 17: На четыре вопроса отвечают по порядку и выбирают продолжение только если во всех уверены.
Особенно у долго живущих приложений, приложений мониторинга, служб и связи с оборудованием нередко жить в сломанном состоянии опаснее, чем честно упасть.
Обработка исключений — это не «искусство никогда не падать». Это проектирование, при котором масштаб порчи минимален, при поломке приложение честно останавливается, а восстановиться легко.
11. Справочные материалы
- .NET: Best practices for exceptions
- .NET: Create a Windows Service using BackgroundService (поведение по умолчанию
BackgroundServiceExceptionBehavior.StopHostаккуратно останавливает хост, поэтому сторона управления службами Windows не перезапускает; чтобы заработали операции восстановления, нужно вызватьEnvironment.Exitс ненулевым кодом) - .NET: System.Exception
- .NET: StackOverflowException
- .NET: System.AccessViolationException
- .NET: Environment.FailFast
- .NET: AppDomain.UnhandledException
- WPF: Application.DispatcherUnhandledException
- Windows Forms: Application.SetUnhandledExceptionMode
- .NET: Exceptions in Managed Threads
- .NET: TaskScheduler.UnobservedTaskException
- Windows: NTSTATUS Values - MS-ERREF
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Журнал событий Windows и ETW: как вывести логи бизнес-приложения в стандартные механизмы ОС
Журнал событий и ETW — отдельный слой записей, который видят специалисты по эксплуатации и стандартные средства ОС. Разбираем, как раздел...
Как устроена изоляция сеансов Windows — Session 0, RDP и одновременная работа нескольких пользователей
Разбираем понятие «сеанса», которое часто путает разработчиков Windows-приложений. С практической стороны объясняем, почему изоляция Sess...
Где в обработке исключений ставить catch и логирование
Чтобы не ловить всё подряд в глубоких helper-ах, не плодить одинаковые логи на каждом слое и не прятать причину за возвращаемым результат...
Где провести границу между юнит-тестами и интеграционными тестами
Границу между юнит-тестами и интеграционными тестами разбираем по осям чистой логики, форматов, связки, различий среды и зависимости от в...
Как в Windows-приложении вынести только операции, которым нужны права администратора
Разбираем, как оставить UI Windows-приложения asInvoker и вынести операции с правами администратора в helper EXE: UAC, runas, именованные...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Технические консультации и ревью дизайна
Речь о том, как выстроить политику обработки исключений, границы отказа, стратегию перезапуска и критерии «продолжать или нет», поэтому тема хорошо стыкуется с технической консультацией и ревью архитектуры.
Расследование ошибок и причин
Отделить, стоит ли после неожиданного исключения продолжать или завершать, с учётом порчи состояния и внешних побочных эффектов, удобно вести как расследование сбоя и разбор первопричины.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Нельзя ли ловить неожиданное исключение через catch (Exception), проглотить его и продолжить?
- Продолжать, только записав в лог, в большинстве случаев опасно: это прячет причину и легко продлевает жизнь уже сломанному состоянию. Продолжать можно, когда одновременно выполняются три условия: сбойную единицу можно отбросить, общее состояние можно вернуть, внешние побочные эффекты можно объяснить. Ось решения — не «можно ли поймать это исключение», а «можно ли после этого ещё доверять состоянию приложения».
- После каких исключений нужно сразу завершать процесс?
- Исключения, которые ставят под сомнение здоровье всего процесса — StackOverflowException, AccessViolationException, тяжёлый OutOfMemoryException, — безопаснее не рассматривать как повод продолжать. При StackOverflowException стек вызовов уже сломан, AccessViolationException — это недопустимый доступ к защищённой памяти, и подозревают порчу памяти. Аномалии, которые стартуют с callback COM, P/Invoke или vendor SDK, тоже сильнее склоняют к завершению: по одному managed-коду безопасность оценить трудно.
- При каких условиях после исключения можно продолжать работу приложения?
- Предпосылка — что примерно одновременно выполняются такие условия: единица сбоя ясна (понятно, что отбросить: одну операцию, один экран, одно задание, одно соединение); состояние можно выбросить и собрать заново; заражение не расползается по общему состоянию; внешние побочные эффекты можно объяснить; пользователю можно честно сказать, что «эта операция не удалась»; по логам и метрикам можно потом разобрать, что случилось. Если граница обработки чёткая, как у одной операции UI или одного задания импорта, продолжение иногда возможно. Наоборот, аномалии посреди обновления общего состояния, в родительском цикле, при запуске или на native-границе склоняют к завершению.
- В WPF можно продолжить, если в DispatcherUnhandledException поставить Handled = true?
- Продолжить после необработанного исключения технически можно, но «можно продолжить» и «безопасно продолжать» — разные вещи. Обработчики вроде AppDomain.UnhandledException и DispatcherUnhandledException полезны как место последней записи, но не как волшебная точка восстановления. В итоге стабильнее собрать сведения об исключении, контекст операции и путь снятия дампа, чтобы разбирать уже после падения. Особенно у долго живущих служб и приложений мониторинга чаще безопаснее и проще диагностировать падение с перезапуском, чем жизнь в наполовину сломанном состоянии.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.