RustMiddleTechnical
Как организовать error handling в приложении через thiserror, anyhow и custom error types?
thiserror автоматически генерирует Display и From для типизированных enum-ошибок библиотек. anyhow даёт ergonomic Box<dyn Error> для приложений с контекстом через .context(). Комбинация: библиотеки используют thiserror, бинарники — anyhow.
Error handling в Rust: thiserror, anyhow и кастомные типы
Кастомные типы без зависимостей
Базовый подход — enum с вариантами ошибок, реализующий std::error::Error, Display и From для конвертации.
use std::fmt;
use std::num::ParseIntError;
use std::io;
#[derive(Debug)]
pub enum ConfigError {
Io(io::Error),
Parse(ParseIntError),
MissingKey(String),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "IO error: {}", e),
ConfigError::Parse(e) => write!(f, "parse error: {}", e),
ConfigError::MissingKey(k) => write!(f, "missing key: {}", k),
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::Io(e) => Some(e),
ConfigError::Parse(e) => Some(e),
ConfigError::MissingKey(_) => None,
}
}
}
impl From<io::Error> for ConfigError {
fn from(e: io::Error) -> Self { ConfigError::Io(e) }
}
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self { ConfigError::Parse(e) }
}
thiserror: для библиотечных ошибок
Крейт thiserror генерирует весь boilerplate через derive-макросы. Версия 1.x стабильна, 2.x — с улучшенной диагностикой.
// Cargo.toml: thiserror = "2"
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("not found: {id}")]
NotFound { id: u64 },
#[error("validation failed: {field} — {message}")]
Validation { field: String, message: String },
#[error("IO error")]
Io(#[from] std::io::Error),
}
async fn get_user(id: u64, pool: &sqlx::PgPool) -> Result<User, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id as i64)
.fetch_optional(pool)
.await?; // sqlx::Error -> AppError::Database через From
user.ok_or(AppError::NotFound { id })
}
anyhow: для приложений и бинарников
anyhow::Error — динамический тип ошибки с backtrace и контекстом. Идеален для main(), CLI и мест, где тип ошибки не важен вызывающему коду.
// Cargo.toml: anyhow = "1"
use anyhow::{Context, Result, bail, ensure};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from '{}'", path))?;
let config: Config = serde_json::from_str(&content)
.context("invalid JSON in config file")?;
ensure!(config.port > 1024, "port must be > 1024, got {}", config.port);
Ok(config)
}
fn main() -> Result<()> {
let config = load_config("config.json")?;
println!("Listening on port {}", config.port);
Ok(())
}
// bail! — ранний возврат с ошибкой
fn check_age(age: u32) -> Result<()> {
if age < 18 {
bail!("age must be >= 18, got {}", age);
}
Ok(())
}
Стратегия: библиотека + приложение
// В lib.rs: thiserror для типизированных ошибок
#[derive(Debug, thiserror::Error)]
pub enum LibError {
#[error("resource not found: {0}")]
NotFound(String),
}
// В main.rs / handlers: anyhow для оркестрации
use anyhow::Result;
fn run() -> Result<()> {
let result = my_lib::do_something()
.context("failed during startup")?; // LibError -> anyhow::Error
Ok(())
}
Подводные камни
- Смешивание
anyhow::Errorв публичном API библиотеки скрывает типы ошибок от пользователей — они не смогут сматчить варианты. #[from]в thiserror создаётFrom-имплементацию; два варианта с одним и тем же#[from]-типом вызовут конфликт.- Backtrace в
anyhowтребуетRUST_BACKTRACE=1илиRUST_LIB_BACKTRACE=1— без них трассировки нет даже в debug-сборке. - Не применяйте
.unwrap()в production-библиотечном коде — паника пересекает crate-границу без контекста. anyhow::Errorне реализуетstd::error::Errorнапрямую — нельзя передать в API, ожидающийBox<dyn Error>, без.into().- Цепочка
source()обрывается, если промежуточный тип не реализуетError::source()— проверяйте полноту цепочки при отладке.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.