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, остальные разбираются в тренажёре.
- Что генерирует thiserror по атрибутам #[error("...")] и #[from]?A)Конструкторы для каждого варианта перечисленияB)Реализацию Serialize, чтобы ошибку можно было отдать по сетиC)Логирование каждой созданной ошибки через макрос logD)Реализации Display, Error и From для помеченных вариантов
показать ответ и разбор
+D)Реализации Display, Error и From для помеченных вариантов// разбор: Это derive-макрос вокруг обычного enum: строка из #[error] превращается в Display с подстановкой полей, поле с #[from] даёт реализацию From и попадает в source. Никакого рантайма он не добавляет — на выходе тот же ручной impl, только не написанный руками.
- Приложение вернуло anyhow::Error, а в одном месте нужно отличить «не найдено» от прочих сбоев. Как быть?A)Сравнить текст ошибки со строкой «не найдено»B)downcast_ref к конкретному типу ошибкиC)Никак: anyhow стирает тип безвозвратноD)Обернуть вызов в catch_unwind и разобрать причину паники
показать ответ и разбор
+B)downcast_ref к конкретному типу ошибки// разбор: anyhow не стирает тип, а прячет его за трейт-объектом: downcast_ref::<MyError>() возвращает Some, если внутри лежит именно она, и работает по всей цепочке причин. Сравнение текстов — хрупкий путь: сообщение поменяют, и ветка тихо перестанет срабатывать.
- Почему для ошибок берут отдельный крейт, а не пишут impl Error руками?A)Ручные Display, Error и From — шаблонный код на каждый вариантB)Стандартная библиотека не даёт трейта для ошибок приложенияC)Ручную реализацию Error компилятор считает устаревшейD)Без крейта оператор ? для своих ошибок не работает
показать ответ и разбор
+A)Ручные Display, Error и From — шаблонный код на каждый вариант// разбор: Руками всё пишется и работает — просто на каждый вариант нужен рукав в Display, реализация From для источника и метод source. На десятке вариантов это страницы однообразного кода, который легко рассинхронизировать. Макрос убирает рутину, семантика остаётся ровно та же.
- Сервис отдаёт клиенту HTTP-статусы. Где мапить свою ошибку в код ответа?A)В самом enum ошибок: пусть каждый вариант хранит свой статусB)В обработчике каждого запроса вручную, ветками по вариантуC)На границе транспорта: домен не знает про HTTPD)В middleware по тексту сообщения об ошибке
показать ответ и разбор
+C)На границе транспорта: домен не знает про HTTP// разбор: Домен формулирует, что произошло (NotFound, Conflict, Invalid), транспортный слой решает, как это выглядит снаружи — в веб-фреймворках это реализация IntoResponse или From для типа ответа. Так одна и та же ошибка обслуживает и HTTP, и очередь, и CLI, а смена протокола не расползается по бизнес-логике.
- Что означает #[error(transparent)] у варианта в thiserror?A)Вариант не попадает в публичную документацию типа ошибкиB)Display и source делегируются вложенной ошибке без добавления своего текстаC)Вариант сериализуется в JSON как обычная строка, без имени варианта и вложенных полейD)Вариант помечается устаревшим и вызывает предупреждение при использовании
показать ответ и разбор
+B)Display и source делегируются вложенной ошибке без добавления своего текста// разбор: Прозрачный вариант — это чистая обёртка: свой текст он не добавляет, а Display и source берёт у вложенной ошибки. Нужно это, когда ваш тип просто пробрасывает чужую ошибку наверх и добавить к ней нечего — иначе в логе появится бессмысленный повтор вроде «ошибка сервиса: ошибка базы: соединение закрыто». Для вариантов, где контекст есть, пишут обычную строку формата.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.