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

thiserror и anyhow

thiserror и anyhow

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

Стержень: thiserror - для типов ошибок библиотеки, anyhow - для сквозной ошибки приложения с контекстом.

// Формулировки: «thiserror или anyhow?», «что делает context?», «как разобрать anyhow::Error?»

Разделение ролей

Библиотека обязана дать пользователю разбираемый тип: чтобы тот мог отличить NotFound от Timeout и построить свою логику. thiserror убирает рутину - по атрибутам над enum он генерирует Display, Error и From, оставляя семантику ровно той же, что при ручном impl.

Приложению чаще нужно другое: один тип ошибки на всё, чтобы ? работал в любой функции, плюс человеческий контекст в логе. Это anyhow. Обратное сочетание - anyhow в публичном API библиотеки - считается плохим тоном: пользователь теряет возможность разобрать причину через match.

// Никакого рантайм-оверхеда thiserror не добавляет: на выходе тот же impl, только не написанный руками.

#[derive(thiserror::Error, Debug)]
enum AppError {
    #[error("не найдено")]
    NotFound,
    #[error("io: {0}")]
    Io(#[from] std::io::Error),
}
thiserror
derive-макрос для типов ошибок библиотеки
anyhow
универсальная ошибка приложения со стёртым типом

Контекст и обратный разбор

Метод .context("чтение конфига") надстраивает слой описания поверх ошибки, сохраняя исходную причину. В логе это печатается лесенкой Caused by, и видно и что делали, и на чём споткнулись. Ленивый вариант - with_context, он вычисляет сообщение только при ошибке.

Иногда в одном месте всё-таки нужно отличить конкретную ошибку. anyhow не выбрасывает тип, а прячет его: downcast_ref::<MyError>() пройдёт по цепочке причин и вернёт Some, если она там есть. Сравнивать текст сообщения для этого - плохая идея: тексты меняются, и ветка тихо перестаёт срабатывать.

// Если такая потребность возникает часто, это сигнал: данному слою пора завести собственный enum ошибок вместо универсального типа.

context
слой описания поверх ошибки с сохранением причины
downcast_ref
приведение стёртой ошибки к конкретному типу

Как отвечать: «thiserror или anyhow - что где?»

В библиотеке - thiserror: пользователю нужен разбираемый enum ошибок, чтобы отличить NotFound от Timeout и построить свою логику. Макрос просто убирает шаблонный код: генерирует Display, Error и From по атрибутам, семантика та же, что при ручной реализации. В приложении - anyhow: там удобнее один сквозной тип, который принимает любую ошибку и позволяет добавлять контекст через .context, а в логе получается цепочка Caused by. Наоборот делать не стоит: anyhow в публичном API лишает пользователей возможности разобрать причину. А если внутри приложения понадобилось отличить конкретную ошибку - есть downcast_ref по цепочке причин.

Ты формулируешь правило по границе «библиотека - приложение», объясняешь, почему обратное плохо, и знаешь про downcast. Это полный ответ без воды.

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

  • Тащат anyhow в публичный API библиотеки.
  • Считают, что thiserror добавляет накладные расходы в рантайме - он их не добавляет.
  • Разбирают ошибку сравнением текста сообщения вместо downcast или своего enum.
  • Пишут context везде, включая горячий путь, забывая про ленивый with_context.
  • Не могут объяснить, зачем вообще нужен крейт, если impl Error пишется руками.

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

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

  1. #rs_error_libs1 / 5
    Что генерирует thiserror по атрибутам #[error("...")] и #[from]?
    A)Конструкторы для каждого варианта перечисления
    B)Реализацию Serialize, чтобы ошибку можно было отдать по сети
    C)Логирование каждой созданной ошибки через макрос log
    D)Реализации Display, Error и From для помеченных вариантов
    показать ответ и разбор
    +D)Реализации Display, Error и From для помеченных вариантов

    // разбор: Это derive-макрос вокруг обычного enum: строка из #[error] превращается в Display с подстановкой полей, поле с #[from] даёт реализацию From и попадает в source. Никакого рантайма он не добавляет — на выходе тот же ручной impl, только не написанный руками.

  2. #rs_error_libs2 / 5
    Приложение вернуло anyhow::Error, а в одном месте нужно отличить «не найдено» от прочих сбоев. Как быть?
    A)Сравнить текст ошибки со строкой «не найдено»
    B)downcast_ref к конкретному типу ошибки
    C)Никак: anyhow стирает тип безвозвратно
    D)Обернуть вызов в catch_unwind и разобрать причину паники
    показать ответ и разбор
    +B)downcast_ref к конкретному типу ошибки

    // разбор: anyhow не стирает тип, а прячет его за трейт-объектом: downcast_ref::<MyError>() возвращает Some, если внутри лежит именно она, и работает по всей цепочке причин. Сравнение текстов — хрупкий путь: сообщение поменяют, и ветка тихо перестанет срабатывать.

  3. #rs_error_libs3 / 5
    Почему для ошибок берут отдельный крейт, а не пишут impl Error руками?
    A)Ручные Display, Error и From — шаблонный код на каждый вариант
    B)Стандартная библиотека не даёт трейта для ошибок приложения
    C)Ручную реализацию Error компилятор считает устаревшей
    D)Без крейта оператор ? для своих ошибок не работает
    показать ответ и разбор
    +A)Ручные Display, Error и From — шаблонный код на каждый вариант

    // разбор: Руками всё пишется и работает — просто на каждый вариант нужен рукав в Display, реализация From для источника и метод source. На десятке вариантов это страницы однообразного кода, который легко рассинхронизировать. Макрос убирает рутину, семантика остаётся ровно та же.

  4. #rs_error_libs4 / 5
    Сервис отдаёт клиенту HTTP-статусы. Где мапить свою ошибку в код ответа?
    A)В самом enum ошибок: пусть каждый вариант хранит свой статус
    B)В обработчике каждого запроса вручную, ветками по варианту
    C)На границе транспорта: домен не знает про HTTP
    D)В middleware по тексту сообщения об ошибке
    показать ответ и разбор
    +C)На границе транспорта: домен не знает про HTTP

    // разбор: Домен формулирует, что произошло (NotFound, Conflict, Invalid), транспортный слой решает, как это выглядит снаружи — в веб-фреймворках это реализация IntoResponse или From для типа ответа. Так одна и та же ошибка обслуживает и HTTP, и очередь, и CLI, а смена протокола не расползается по бизнес-логике.

  5. #rs_error_libs5 / 5
    Что означает #[error(transparent)] у варианта в thiserror?
    A)Вариант не попадает в публичную документацию типа ошибки
    B)Display и source делегируются вложенной ошибке без добавления своего текста
    C)Вариант сериализуется в JSON как обычная строка, без имени варианта и вложенных полей
    D)Вариант помечается устаревшим и вызывает предупреждение при использовании
    показать ответ и разбор
    +B)Display и source делегируются вложенной ошибке без добавления своего текста

    // разбор: Прозрачный вариант — это чистая обёртка: свой текст он не добавляет, а Display и source берёт у вложенной ошибки. Нужно это, когда ваш тип просто пробрасывает чужую ошибку наверх и добавить к ней нечего — иначе в логе появится бессмысленный повтор вроде «ошибка сервиса: ошибка базы: соединение закрыто». Для вариантов, где контекст есть, пишут обычную строку формата.

дальше

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

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