Представьте, что функция должна изолировать аварийное завершение замыкаемой операции. Какой результат выведет программа и за счёт какого механизма выполнение продолжается после panic!?
use std::panic::catch_unwind;
fn main() {
let result = catch_unwind(|| {
panic!("сбой");
});
println!("{}", result.is_err());
println!("продолжение");
}
Программа выведет true, затем продолжение. catch_unwind перехватывает панику, если она распространяется по стеку в режиме unwind, и превращает её в Err типа Result вместо немедленного завершения потока.
Это не превращает любую панику в обычную ошибку: при стратегии panic = "abort" размотки стека нет, поэтому catch_unwind не сможет продолжить выполнение.
В Rust разделены два класса отказов. Result предназначен для ожидаемых ошибок, которые вызывающий код должен обработать, а panic! — для нарушения инварианта или ситуации, которую текущий код не может корректно обработать.
Стратегия размотки стека позволяет перед завершением паники выполнить очистку локальных значений и, при необходимости, перехватить панику на специально выбранной границе. Альтернативная стратегия прерывания процесса проще и быстрее, но не предоставляет такой точки перехвата.
Если библиотека или инфраструктурный слой вызывает потенциально аварийный пользовательский код напрямую, его panic! может прервать текущий поток и сделать недоступным код после вызова. Это особенно опасно на границах задач, плагинов или обработчиков запросов, где один изолированный сбой не должен автоматически разрушать окружающий контролируемый контекст.
Однако безусловный перехват паники тоже рискован: он может скрыть ошибку программирования, оставить состояние приложения логически повреждённым или создать ложное впечатление, что операция безопасно восстановлена.
catch_unwind принимает замыкание и возвращает Result<T, Box<dyn Any + Send>>. Если замыкание завершается обычным значением, возвращается Ok(T); если внутри него происходит unwind-паника, размотка стека останавливается на границе catch_unwind, а её payload помещается в Err.
В примере замыкание имеет результат (), поэтому тип результата фактически соответствует Result<(), Box<dyn Any + Send>>. Метод is_err() печатает true, после чего main продолжает выполнение и печатает вторую строку.
Перехват не означает, что паника стала обычной доменной ошибкой. Payload может содержать строку, число или другой тип, а не только String; его извлечение требует проверки через downcast_ref или downcast.
catch_unwind работает только при стратегии размотки. При panic = "abort" процесс немедленно завершается, деструкторы при размотке не вызываются, а catch_unwind не получает управление. Кроме того, граница требует замыкание, удовлетворяющее ограничению UnwindSafe; AssertUnwindSafe снимает проверку компилятора, но не исправляет потенциально неконсистентное состояние.
Перехватывать следует только на осмысленной границе, где есть политика восстановления, журналирования или изоляции. Для ожидаемого отказа операции нужно возвращать Result, а не использовать panic! с последующим catch_unwind.
Минимальный пример механизма:
Сервис запускает пользовательские обработчики внутри рабочего потока. Один обработчик может содержать ошибку программирования и вызвать панику, но процесс должен продолжить обслуживать остальные задачи.
Вариант без перехвата прост и сохраняет видимость ошибки, но паника разрушает поток, а при отсутствии других рабочих потоков — и полезность сервиса. Вариант с глобальным подавлением паник скрывает проблему и не создаёт надёжной границы восстановления. Вариант с catch_unwind вокруг одного обработчика позволяет записать payload в журнал, пометить задачу как аварийно завершённую и продолжить цикл.
Выбранное решение — локальный catch_unwind на границе запуска обработчика, при этом сам обработчик использует Result для ожидаемых ошибок. Это разделяет ошибки бизнес-операции и дефекты реализации; после перехвата система всё равно должна считать состояние обработчика потерянным и не продолжать его внутреннюю транзакцию.
Можно ли считать catch_unwind аналогом try/catch из языков с исключениями?
Нет. panic! не является обычным механизмом передачи ожидаемых ошибок. Перехват предназначен для ограниченных границ отказа, а не для управления штатным потоком выполнения. В отличие от обычного Result, тип ошибки не описывает заранее конкретную бизнес-причину, и восстановление после паники может быть небезопасным из-за нарушенных инвариантов.
Почему catch_unwind иногда не компилируется для замыкания, захватывающего изменяемое состояние?
Замыкание проверяется на UnwindSafe. Компилятор пытается не допустить ситуацию, при которой паника произойдёт посреди изменения структуры данных, а после перехвата код продолжит использовать её частично обновлённое состояние. AssertUnwindSafe можно применить, если разработчик вручную доказал безопасность, но это лишь обещание компилятору; необходимо отдельно обеспечить восстановление или выбросить повреждённое состояние.
Что изменится при настройке panic = "abort"?
При abort паника не разматывает стек и не вызывает обработчики, рассчитывающие на catch_unwind; процесс завершается немедленно. Такая настройка может уменьшить размер бинарного файла и упростить модель завершения, но исключает локальную изоляцию паник. Если приложению нужно продолжать работу после паники конкретного обработчика, должна использоваться стратегия unwind, а граница перехвата должна быть спроектирована явно.