Задача Tokio завершилась паникой: каким образом эта паника достигает вызывающей async-задачи через JoinHandle?
Паника внутри задачи Tokio не раскручивает стек вызывающей async-задачи. Runtime перехватывает её на границе задачи, а JoinHandle::await возвращает ошибку JoinError, по которой можно определить панику и при необходимости повторно вызвать её в текущей задаче.
Если JoinHandle уничтожить, не ожидая его, вызывающая задача не получит эту ошибку: задача будет отсоединена, а её результат останется ненаблюдаемым.
Асинхронный runtime выполняет множество независимых задач внутри ограниченного числа потоков. Поэтому паника одной задачи не должна автоматически разрушать поток исполнителя и все остальные задачи, как это произошло бы при обычном неперехваченном panic! на уровне потока.
Модель JoinHandle отделяет выполнение задачи от наблюдения за её результатом. Она решает ту же практическую проблему, что и результат join у потока: вызывающий код явно выбирает, проверять ли успешное завершение, ошибку или панику.
Async-задача может завершиться тремя существенно разными способами: вернуть значение, завершиться ошибкой через собственный тип результата или аварийно завершиться паникой. Если различать только внешний результат задачи, можно ошибочно принять панику за обычную прикладную ошибку или вовсе потерять её.
Особенно опасен вызов unwrap на результате JoinHandle: он превратит ошибку присоединения в новую панику уже в вызывающей задаче. Это допустимо для фатальной ошибки, но плохо подходит для серверного кода, где нужно сохранить работоспособность остальных запросов.
JoinHandle<T> при ожидании возвращает примерно такую логическую форму: успешное значение T либо JoinError. Если задача вызвала панику, JoinError::is_panic() возвращает true; сам объект ошибки позволяет получить payload паники через into_panic() или повторно распространить её с помощью resume_unwind.
Перехватывается именно паника внутри границы spawned-задачи. Это не означает, что runtime исправляет состояние, изменённое задачей до паники: общие данные могли остаться частично обновлёнными, поэтому их инварианты должны защищаться отдельно.
Минимальный пример обработки:
Вызов abort завершает задачу отменой, если она ещё может быть остановлена runtime; это отдельный сценарий и не превращает панику в отмену. Уничтожение JoinHandle также не отменяет задачу автоматически: оно отсоединяет её, поэтому вызывающий код больше не получает результат.
Преимущество перехвата паники — изоляция отказа и возможность централизованно записать диагностику. Недостаток — паника может быть замечена слишком поздно или не замечена вообще, если handle отброшен; кроме того, продолжение работы после частичного изменения общего состояния может быть небезопасным.
В HTTP-сервисе отдельная async-задача обрабатывает независимый фоновой job и иногда вызывает панику из-за ошибки инварианта. Возможны три варианта: вызвать unwrap, отбросить handle или явно обработать JoinError.
unwrap прост, но переносит панику в задачу-диспетчер. Отбрасывание handle не мешает фоновому job продолжить выполнение и скрывает его отказ. Явное ожидание позволяет записать диагностику, отличить панику от отмены и решить, нужно ли остановить сервис или пометить конкретный job как неуспешный.
Практически выбирают явную обработку JoinError, а после паники обычно прекращают дальнейшую работу с потенциально повреждённым состоянием job. Это сохраняет живучесть runtime, но не маскирует программную ошибку.
1. Вопрос: равны ли паника внутри задачи Tokio и паника вызывающей async-задачи?
Нет. Паника задачи Tokio изолируется runtime и представляется как ошибка при ожидании JoinHandle. Паника вызывающей задачи происходит в её собственном контексте и не становится автоматически ошибкой другого handle.
2. Вопрос: что произойдёт, если вызвать unwrap на результате JoinHandle после паники дочерней задачи?
unwrap увидит Err(JoinError) и сам запаникует в вызывающей задаче. Таким образом, исходная паника не «передаётся» обычным раскручиванием стека; она преобразуется в данные, а затем может быть намеренно превращена обратно в панику.
3. Вопрос: гарантирует ли перехват паники, что общие данные остались корректными?
Нет. Перехватывается управление, а не последствия уже выполненных операций. Если задача изменила несколько связанных полей и запаниковала между изменениями, логический инвариант мог нарушиться; для защиты нужны транзакционная логика, корректная синхронизация и восстановление состояния.