сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Ошибки в Rust

panic, unwrap и expect в Rust

Паника: когда и что происходит

Про панику спрашивают, чтобы понять твоё чувство границы: что считать багом, а что нормальным сбоем. Плюс механика - unwind против abort, поведение в потоках, отравление мьютексов.

Стержень: паника это сигнал о нарушенном инварианте программы, а не способ вернуть ошибку.

// Формулировки: «когда допустим unwrap?», «что происходит при панике?», «что увидит join, если поток паниковал?»

unwrap, expect и граница применимости

unwrap разворачивает значение, а при Err или None паникует со стандартным текстом. expect делает то же с вашим сообщением, и это почти всегда лучше: в лог попадает нарушенное ожидание, а не «called unwrap on a None value».

Граница простая. Файл, сеть, пользовательский ввод - ожидаемые сбои, им место в Result. Индекс за границей, невозможное состояние, отсутствие обязательного конфига на старте - нарушенный инвариант, и продолжать работу опаснее, чем упасть. Поэтому библиотеки почти всегда возвращают Result: решать, падать ли, должен вызывающий.

// В сообщении expect пишут ожидание, а не диагноз: «конфиг должен быть прочитан на старте» полезнее, чем «ошибка чтения».

unwrap
развернуть значение или паниковать
expect
то же со своим сообщением о нарушенном ожидании

Unwind, abort и границы потоков

По умолчанию паника разворачивает стек: идёт вверх по кадрам, вызывая деструкторы, файлы закрываются, блокировки снимаются. Дойдя до границы потока, она его завершает. Профиль с panic = abort обрывает процесс мгновенно: бинарник меньше, сборка быстрее, но деструкторы не отрабатывают.

Через границу потока паника не проходит: join() возвращает Err с упакованной причиной, а процесс продолжает жить. Поэтому пул воркеров переживает падение одной задачи. Побочный эффект - отравление: мьютекс, удерживаемый упавшим потоком, помечается, и следующий lock() возвращает Err.

// catch_unwind ловит панику внутри потока это страховка на границе сервера, чтобы один запрос не уронил процесс. Механизмом обработки ошибок он не является: состояние после паники под вопросом, а в режиме abort ловить вообще нечего.

unwind
разворачивание стека с вызовом деструкторов
отравление
пометка мьютекса после паники под блокировкой

Как отвечать: «Когда unwrap в проде допустим?»

Когда нарушение условия означает баг, а не внешний сбой. Разбор зашитой в код константы, обязательный конфиг на старте сервиса, инварианты внутри модуля, тесты - там паника уместна, потому что продолжать работу с испорченным состоянием опаснее, чем упасть. Всё, что зависит от внешнего мира - ввод пользователя, сеть, файлы, база, обязано возвращать Result. И даже там, где паника уместна, я пишу expect с сообщением про нарушенное ожидание: по логу сразу видно, какое условие сломалось.

Ты даёшь критерий вместо списка правил и добавляешь практическую деталь про expect. Это ответ, по которому видно, что человек читал свои логи в проде.

На чём валятся

  • Ставят unwrap на пользовательский ввод и сетевые вызовы.
  • Считают catch_unwind заменой обработке ошибок.
  • Думают, что паника валит процесс. Она валит поток; процесс умирает, только если это main.
  • Забывают, что при panic = abort деструкторы не отработают и буферы не сбросятся.
  • Не знают про отравление мьютекса и глушат его unwrap, не подумав о целостности данных.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_panic1 / 5
    Что происходит при панике по умолчанию (panic = "unwind")?
    A)Стек разворачивается, деструкторы отрабатывают
    B)Поток засыпает, а обработчик паники решает, продолжать ли
    C)Процесс немедленно завершается без вызова деструкторов
    D)Паника превращается в Err и возвращается вызывающей функции
    показать ответ и разбор
    +A)Стек разворачивается, деструкторы отрабатывают

    // разбор: Разворачивание идёт по кадрам стека вверх и вызывает Drop для всех живых значений — файлы закрываются, блокировки снимаются. Дойдя до границы потока, паника его завершает. Альтернатива panic = abort экономит код и время сборки, но обрывает процесс мгновенно, деструкторы не отрабатывают.

  2. #rs_panic2 / 5
    Рабочий поток паникует. Что увидит поток, который его ждёт через join()?
    A)Панику в своём потоке: она пробрасывается через границу
    B)Ok со значением по умолчанию для типа результата
    C)Err с упакованной причиной паники — процесс при этом жив
    D)Ничего: join заблокируется навсегда, поток не завершился штатно
    показать ответ и разбор
    +C)Err с упакованной причиной паники — процесс при этом жив

    // разбор: Паника не переходит границу потока: она разворачивает свой стек и превращается в Err(Box<dyn Any>) на стороне join. Главный поток решает, что делать — повторить работу, залогировать, упасть самому. Именно поэтому пул воркеров переживает панику отдельной задачи, а мьютекс, захваченный упавшим потоком, помечается отравленным.

  3. #rs_panic3 / 5
    Веб-сервер ловит панику обработчика через catch_unwind. Почему это не замена обработке ошибок?
    A)catch_unwind не компилируется в многопоточном коде
    B)Перехват паники стоит дороже, чем возврат Result
    C)После перехвата процесс всё равно завершится через несколько секунд
    D)Состояние после паники не гарантировано, а abort не поймать
    показать ответ и разбор
    +D)Состояние после паники не гарантировано, а abort не поймать

    // разбор: catch_unwind — страховка на границе (не уронить весь сервер из-за одного запроса), а не механизм управления потоком. Инварианты после паники могли поплыть, замок мог остаться отравленным, а сборка с panic = abort вообще не даст ничего поймать. Ожидаемые сбои по-прежнему возвращают Result.

  4. #rs_panic4 / 5
    Где unwrap в проде обычно допустим?
    A)В обработчиках HTTP-запросов: паника вернёт клиенту 500
    B)Там, где нарушение условия — это баг: константа, старт сервиса
    C)При разборе пользовательского ввода: неверные данные — вина клиента
    D)В коде, покрытом тестами: тесты поймают ошибку заранее
    показать ответ и разбор
    +B)Там, где нарушение условия — это баг: константа, старт сервиса

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

  5. #rs_panic5 / 5
    Что произойдёт при выполнении?
    let v: Vec<i32> = Vec::new();
    println!("{:?}", v.first());
    println!("{}", v[0]);
    A)Паника уже на first(): пустой вектор так не опрашивают
    B)None, затем 0: пустой вектор отдаёт значение по умолчанию
    C)None, затем паника: индекс за границей
    D)Не соберётся: компилятор видит индексацию пустого вектора
    показать ответ и разбор
    +C)None, затем паника: индекс за границей

    // разбор: first возвращает Option и на пустом векторе честно отдаёт None — отсутствие элемента здесь ожидаемый исход, а не ошибка. Индексация же обещает элемент, поэтому при выходе за границу паникует: index out of bounds, the len is 0 but the index is 0. Длину компилятор в общем случае не знает, а безопасный аналог индексации — get(0), который тоже вернёт None.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.