Начало работы
Эта страница проведёт вас через установку masterror, возврат первого AppError, ранние возвраты с ensure!/fail!, использование прелюдии и написание первого derive.
Установка
Сборка по умолчанию включает только функцию std — без веб-фреймворков и бэкендов телеметрии:
[dependencies]
masterror = "0.28"
Включайте интеграции по мере необходимости (полный список — в Флагах возможностей):
[dependencies]
masterror = { version = "0.28", features = ["axum", "serde_json", "tracing"] }
MSRV — 1.96. Крейт запрещает unsafe и поддерживает no_std при сборке с default-features = false.
Ваша первая ошибка
AppError объединяет семантическую категорию (AppErrorKind) с опциональным публичным сообщением. AppResult<T> — псевдоним для Result<T, AppError>:
use masterror::{AppError, AppErrorKind, AppResult};
fn do_work(flag: bool) -> AppResult<()> {
if !flag {
return Err(AppError::new(AppErrorKind::BadRequest, "Flag must be set"));
}
Ok(())
}
let err = do_work(false).unwrap_err();
assert!(matches!(err.kind, AppErrorKind::BadRequest));
assert_eq!(err.kind.http_status(), 400);
У каждого вида есть именованный конструктор, так что в местах вызова редко приходится писать AppErrorKind явно:
use masterror::AppError;
let _ = AppError::not_found("user not found"); // 404
let _ = AppError::validation("invalid email"); // 422
let _ = AppError::unauthorized("token expired"); // 401
let _ = AppError::forbidden("no access"); // 403
let _ = AppError::conflict("already exists"); // 409
let _ = AppError::rate_limited("slow down"); // 429
let _ = AppError::internal("unexpected failure"); // 500
let _ = AppError::service("orchestration failed"); // 500
let _ = AppError::timeout("upstream timed out"); // 504
let _ = AppError::bare(masterror::AppErrorKind::NotFound); // no message
Прикрепляйте структурированные метаданные и вышестоящий источник, не отказываясь от типизации:
use masterror::{AppError, field};
let err = AppError::service("downstream degraded")
.with_field(field::str("request_id", "abc123"))
.with_field(field::i64("attempt", 2))
.with_context(std::io::Error::other("connection reset"));
assert_eq!(err.metadata().len(), 2);
assert!(err.source_ref().is_some());
Источник доступен для логов и обхода через chain(), но никогда не сериализуется для клиентов.
ensure! и fail!
ensure! и fail! — типизированные альтернативы anyhow::ensure!/anyhow::bail!. Выражение ошибки вычисляется лениво, поэтому на успешном пути нет ни форматирования, ни выделения памяти:
use masterror::{AppError, AppErrorKind, AppResult};
fn guard(flag: bool) -> AppResult<()> {
masterror::ensure!(flag, AppError::bad_request("flag must be set"));
Ok(())
}
fn bail() -> AppResult<()> {
masterror::fail!(AppError::unauthorized("token expired"));
}
assert!(guard(true).is_ok());
assert!(matches!(guard(false).unwrap_err().kind, AppErrorKind::BadRequest));
assert!(matches!(bail().unwrap_err().kind, AppErrorKind::Unauthorized));
ensure! также принимает развёрнутую форму для сложных условий:
use masterror::{AppError, AppResult};
fn bounded(value: i32, max: i32) -> AppResult<()> {
masterror::ensure!(
cond = value <= max,
else = AppError::service("value too large")
);
Ok(())
}
Прелюдия
masterror::prelude реэкспортирует только основные типы (AppError, AppErrorKind, AppCode, AppResult, ErrorResponse, а также хелперы turnkey, когда включена соответствующая функция):
use masterror::prelude::*;
fn handler(flag: bool) -> AppResult<()> {
if !flag {
return Err(AppError::bad_request("Flag must be set"));
}
Ok(())
}
Реализации трейтов фреймворков (Axum IntoResponse, Actix Responder) активируются флагами возможностей и не требуют дополнительных импортов.
Добавление контекста к внешним ошибкам
ResultExt превращает любой Result<T, E: Error> в AppResult<T>:
use masterror::{AppErrorKind, Context, ResultExt, field};
fn read_config() -> Result<String, std::io::Error> {
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}
// Simple, anyhow-style message:
let err = read_config().context("Failed to read config file").unwrap_err();
assert!(err.source_ref().is_some());
// Full control over category, code, metadata and redaction:
let err = read_config()
.ctx(|| Context::new(AppErrorKind::Config).with(field::str("path", "app.toml")))
.unwrap_err();
assert_eq!(err.kind, AppErrorKind::Config);
Полный API Context описан в разделе Контекст и метаданные.
Ваш первый derive
#[derive(Error)] повторяет синтаксис thiserror, а #[app_error(...)] добавляет конверсию в AppError:
use masterror::{AppCode, AppError, AppErrorKind, Error};
#[derive(Debug, Error)]
#[error("I/O failed: {source}")]
#[app_error(kind = AppErrorKind::Internal, code = AppCode::Internal, message)]
pub struct DomainError {
#[from]
#[source]
source: std::io::Error
}
fn load() -> Result<(), DomainError> {
Err(std::io::Error::other("disk offline").into())
}
let err = load().unwrap_err();
assert_eq!(err.to_string(), "I/O failed: disk offline");
let app: AppError = err.into();
assert!(matches!(app.kind, AppErrorKind::Internal));
#[error("...")]задаёт шаблонDisplayс плейсхолдерами{field}.#[from]генерируетFrom<std::io::Error>для обёртки.#[source]пробрасывает внутреннюю ошибку черезsource().#[app_error(kind = ..., code = ..., message)]генерируетFrom<DomainError> for AppError(иfor AppCode); флагmessageделает выводDisplayпубличным сообщением.
Enum работают так же — с атрибутами #[error] и #[app_error] для каждого варианта. Когда вам также нужны метаданные, политика редактирования и таблицы отображений gRPC/problem+json, используйте #[derive(Masterror)] — он описан в разделе Derive-макросы.
Куда двигаться дальше
- Отображение ошибок в HTTP-ответы в Axum или Actix — Веб-фреймворки
- Понимание таксономии и wire-контракта — Виды и коды ошибок
- Включение интеграций для sqlx, redis, reqwest — Флаги возможностей
См. также: Флаги возможностей · Виды и коды ошибок · Derive-макросы · Контекст и метаданные · Миграция