Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Миграция

masterror спроектирован как прямая замена одновременно thiserror (синтаксис derive) и anyhow (эргономика). Большинство миграций — это замена зависимости плюс постепенное принятие типизированных возможностей. Запускаемые пошаговые примеры: examples/migrate_from_thiserror.rs и examples/migrate_from_anyhow.rs.

С thiserror

Шаг 1 механический — поменяйте импорт; синтаксис derive совместим:

-use thiserror::Error;
+use masterror::Error;

Совместимость атрибутов

thiserrormasterrorПримечания
#[error("...")] с плейсхолдерами {field}так же1:1, включая позиционные {0}
Спецификаторы формата :>8, :.3, :x, :p, :eтак жеTemplateFormatter повторяет детекцию форматтеров thiserror
#[error(transparent)]так жетребует обёрток с одним полем, пробрасывающих Display/source
#[from]так жегенерирует From<...>, проверяет форму обёртки
#[source]так жеподключает цепочку source()
#[backtrace]так жеучитывается на полях
#[app_error(kind = ..., code = ..., message)]добавлено: генерирует From<YourError> for AppErrorAppCode); message пробрасывает Display как публичное сообщение
#[provide(ref = T, value = T)]добавлено: типизированная телеметрия через std::error::Request; поля Option<T> предоставляют значение только при Some
#[derive(Masterror)] + #[masterror(...)]добавлено: полное отображение с category, redact(message, fields(...)), telemetry(...), map.grpc, map.problem

Существующие enum продолжают компилироваться без изменений. Что даёт их аннотирование:

use masterror::{AppCode, AppError, AppErrorKind, Error};

#[derive(Debug, Error)]
#[error("user {user_id} not found")]
#[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound, message)]
struct UserMissing {
    user_id: u64
}

let app: AppError = UserMissing { user_id: 42 }.into();
assert_eq!(app.kind, AppErrorKind::NotFound);

Enum отображаются повариантно — каждый вариант несёт собственный #[app_error(...)], а derive генерирует единственный From<Enum> for AppError.

Рекомендуемый порядок: (1) поменять импорт, (2) добавить #[app_error] типам, пересекающим границу API, (3) заменить рукописные реализации From<DomainError> for AppError сгенерированными, (4) принять #[masterror(...)] там, где нужны редактирование или метаданные.

С anyhow

anyhowmasterrorПримечания
anyhow::Result<T>masterror::AppResult<T>псевдоним для Result<T, AppError>
anyhow::Errormasterror::AppError / Errorнесёт вид, код и метаданные вместо «блоба»
.context("msg").context("msg")идентично, через masterror::ResultExt
.with_context(|| ...).ctx(|| Context::new(kind)...)ленив, как в anyhow, но строит типизированный Context (вид, код, поля, редактирование)
bail!(err)fail!(err)принимает выражение типизированной ошибки, без машинерии форматирования
ensure!(cond, "msg {x}")ensure!(cond, AppError::...)условие + типизированная ошибка; без форматирования на успешном пути
err.chain()err.chain()тот же итератор по цепочке источников
err.root_cause()err.root_cause()так же
err.is::<E>() / downcast / downcast_ref / downcast_mutте же имена на AppErrorпаритет по даункастингу
Обёртывание в стиле #[from]Реализации From<...> за флагами функцийsqlx/redis/reqwest/… приходят уже классифицированными

.context() работает ровно так, как вы ожидаете:

use masterror::{AppResult, ResultExt};

fn read_config(path: &str) -> AppResult<String> {
    let content = std::fs::read_to_string(path).context("Failed to read config file")?;
    Ok(content)
}

ensure!/fail! меняют строковое форматирование anyhow на типизированные ошибки:

use masterror::{AppError, AppResult, ensure, fail, field};

fn parse(content: &str, path: &str) -> AppResult<()> {
    ensure!(
        !content.is_empty(),
        AppError::bad_request("Config file is empty")
            .with_field(field::str("path", path.to_owned()))
    );
    if content.starts_with("invalid") {
        fail!(AppError::bad_request("Invalid config format"));
    }
    Ok(())
}

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

Где у anyhow нет эквивалента

Миграция даёт возможности, у которых нет аналога в anyhow:

  • Типизированная таксономияAppErrorKind (внутренний) и AppCode (публичный, SCREAMING_SNAKE_CASE) вместо строкового контекста. См. Виды и коды ошибок.
  • Транспортные отображения — RFC 7807 problem+json для Axum/Actix и tonic::Status для gRPC, выводимые из одной таблицы кодов. См. Веб-фреймворки.
  • Телеметрия — автоматические события tracing, метрики error_total{code,category} и ленивые бэктрейсы на границе. См. Наблюдаемость.
  • Редактирование — сообщения redactable() и политики Hash/Last4/Redact для каждого поля, соблюдаемые каждым транспортом. См. Лучшие практики.
  • Структурированные метаданные — типизированные field::str/u64/duration/ip/... вместо форматирования значений в сообщение.

На что обратить внимание

  • У формы anyhow ensure!(cond, "format {}", x) с форматированным сообщением нет прямого двойника: конструируйте ошибку явно (AppError::bad_request(format!(...))) или, что лучше, используйте статическое сообщение плюс поля метаданных.
  • anyhow::Error принимает любой E: Error + Send + Sync. В masterror вы выбираете вид в момент оборачивания (Context::new(kind) или конверсия From) — эта точка принятия решения и есть возможность, а не трение: именно здесь происходит классификация.
  • И ensure!, и fail! разворачиваются в return Err(...), поэтому работают в любой функции, возвращающей Result<_, E>, где ваше выражение уже является типом ошибки — конверсия Into не вставляется.

См. также: Начало работы · Derive-макросы · Контекст и метаданные · Лучшие практики