388 lines · 16.1 KB · Rust
RawDownload
//! Common error types and utilities for error handling.//!//! # Usage//!//! * When there is **no callee error** to track, use *simple* `std::error::Error` implementations directly,//!   e.g. `Result<_, Simple>`.//!      - If call-site tracking is important, prefer `Result<_, Exn<Simple>>` instead://!        [`Exn`] stores the location where the error was raised, which plain error values do not.//! * When there **is callee error to track** *in a `gix-plumbing`*, use e.g. `Result<_, Exn<Simple>>`.//!      - Remember that `Exn<T>` does not implement `std::error::Error` so it's not easy to use outside `gix-` crates.//!      - Use the type-erased version in callbacks like [`Exn`] (without type arguments), i.e. `Result<T, Exn>`.//! * When there **is callee error to track** *in the `gix` crate*, convert both `std::error::Error` and `Exn<E>` into [`Error`]//!//! # Standard Error Types//!//! These should always be used if they match the meaning of the error well enough instead of creating an own//! [`Error`](std::error::Error)-implementing type, and used with//! [`ResultExt::or_raise(<StandardErrorType>)`](ResultExt::or_raise) or//! [`OptionExt::ok_or_raise(<StandardErrorType>)`](OptionExt::ok_or_raise), or sibling methods.//!//! All these types implement [`Error`](std::error::Error).//!//! ## [`Message`]//!//! The baseline that provides a formatted message.//! Formatting can more easily be done with the [`message!`] macro as convenience, roughly equivalent to//! [`Message::new(format!("…"))`](Message::new) or `format!("…").into()`.//!//! ## Specialised types//!//! - [`ValidationError`]//!    - like [`Message`], but can optionally store the input that caused the failure.//!    - For message-only validation failures, use `Into<ValidationError>` conversions instead of spelling out//!      [`ValidationError::new()`]://! ```rust,ignore//! // All of these produce ValidationError://! return Err(message("eof reading amount of bits").into());//! let value = maybe_value.ok_or(message("missing value").into())?;//! let header = maybe_header.ok_or("missing header".into())?;//! ```//!    - Use [`ValidationError::new_with_input()`] when you need to preserve the offending input.//!//! # [`Exn<ErrorType>`](Exn) and [`Exn`]//!//! The [`Exn`] type does not implement [`Error`](std::error::Error) itself, but is able to store causing errors//! via [`ResultExt::or_raise()`] (and sibling methods) as well as location information of the creation site.//!//! While plumbing functions that need to track causes should always return a distinct type like [`Exn<Message>`](Exn),//! if that's not possible, use [`Exn::erased`] to let it return `Result<T, Exn>` instead, allowing any return type.//!//! A side effect of this is that any callee that causes errors needs to be annotated with//! `.or_raise(|| message!("context information"))` or `.or_raise_erased(|| message!("context information"))`.//!//! # Using `Exn` (bare) in closure *bounds*//!//! Callback and closure **bounds** should use `Result<T, Exn>` (bare, without a type parameter)//! rather than `Result<T, Exn<Message>>` or any other specific type. This allows callers to//! return any error type from their callbacks without being forced into `Message`.//!//! Note that functions should still return the most specific type possible (usually `Exn<Message>`);//! only the *bound* on the callback parameter should use the bare `Exn`.//!//! ```rust,ignore//! // GOOD — callback bound is flexible, function return is specific://! fn process(cb: impl FnMut() -> Result<(), Exn>) -> Result<(), Exn<Message>> { ... }//!//! // BAD — forces caller to construct Message errors in their callback://! fn process(cb: impl FnMut() -> Result<(), Exn<Message>>) -> Result<(), Exn<Message>> { ... }//! ```//!//! Inside the function, use [`.or_raise()`](ResultExt::or_raise) to convert the bare `Exn` from the//! callback into the function's typed error, adding context://! ```rust,ignore//! let entry = callback().or_raise(|| message("context about the callback call"))?;//! ```//!//! Inside a closure that must return bare `Exn`, use [`.or_erased()`](ResultExt::or_erased) to//! convert a typed `Exn<E>` to `Exn`, or [`raise_erased()`](ErrorExt::raise_erased) for standalone errors://! ```rust,ignore//! |stream| {//!     stream.next_entry().or_erased()   // Exn<Message> → Exn//! }//! ```//!//! # [`Error`] — `Exn` with `std::error::Error`//!//! Since [`Exn`] does not implement [`std::error::Error`], it cannot be used where that trait is required//! (e.g. `std::io::Error::other()`, or as a `#[source]` in another error type).//! The [`Error`] type bridges this gap: it implements [`std::error::Error`] and converts from any//! [`Exn<E>`](Exn) via [`From`], preserving the full error tree and location information.//!//! ```rust,ignore//! // Convert an Exn to something usable as std::error::Error://! let exn: Exn<Message> = message("something failed").raise();//! let err: gix_error::Error = exn.into();//! let err: gix_error::Error = exn.into_error();//!//! // Useful where std::error::Error is required://! std::io::Error::other(exn.into_error())//! ```//!//! It can also be created directly from any `std::error::Error` via [`Error::from_error()`].//!//! # Migrating from `thiserror`//!//! This section describes the mechanical translation from `thiserror` error enums to `gix-error`.//! In `Cargo.toml`, replace `thiserror = "<version>"` with `gix-error = { version = "^0.1.0", path = "../gix-error" }`.//!//! ## Choosing the replacement type//!//! There are two decisions: whether to wrap in [`Exn`], and which error type to use.//!//! **With or without [`Exn`]:**//!//! | `thiserror` enum shape                                      | Wrap in `Exn`? |//! |--------------------------------------------------------------|----------------|//! | All variants are simple messages (no `#[from]`/`#[source]`)  | No             |//! | Has `#[from]` or `#[source]` (wraps callee errors)           | Yes            |//!//! **Which error type** (used directly or as the `E` in `Exn<E>`)://!//! | Semantics                                                    | Error type            |//! |--------------------------------------------------------------|-----------------------|//! | General-purpose error messages                                | [`Message`]           |//! | Validation/parsing, optionally storing the offending input   | [`ValidationError`]   |//! | Malformed or internally inconsistent data                     | [`CorruptionError`]   |//! | A requested resource does not exist                            | [`NotFoundError`]     |//!//! For example, a validation function with no callee errors returns `Result<_, ValidationError>`,//! while a function that wraps I/O errors during parsing could return `Result<_, Exn<ValidationError>>`.//! When in doubt, [`Message`] is the default choice.//!//! ## Translating variants//!//! The translation depends on the chosen return type. When the function returns a plain error//! type like `Result<_, Message>`, return the error directly. When it returns `Result<_, Exn<_>>`,//! use [`.raise()`](ErrorExt::raise) to wrap the error into an [`Exn`].//!//! **Static message variant:**//! ```rust,ignore//! // BEFORE://! #[error("something went wrong")]//! SomethingFailed,//! // → Err(Error::SomethingFailed)//!//! // AFTER (returning Message)://! // → Err(message("something went wrong"))//!//! // AFTER (returning Exn<Message>)://! // → Err(message("something went wrong").raise())//! ```//!//! **Formatted message variant:**//! ```rust,ignore//! // BEFORE://! #[error("unsupported format '{format:?}'")]//! Unsupported { format: Format },//! // → Err(Error::Unsupported { format })//!//! // AFTER (returning Message)://! // → Err(message!("unsupported format '{format:?}'"))//!//! // AFTER (returning Exn<Message>)://! // → Err(message!("unsupported format '{format:?}'").raise())//! ```//!//! **`#[from]` / `#[error(transparent)]` variant** — delete the variant;//! at each call site, use [`ResultExt::or_raise()`] to add context://! ```rust,ignore//! // BEFORE://! #[error(transparent)]//! Io(#[from] std::io::Error),//! // → something_that_returns_io_error()?  // auto-converted via From//!//! // AFTER (the variant is deleted)://! // → something_that_returns_io_error()//! //       .or_raise(|| message("context about what failed"))?//! ```//!//! **`#[source]` variant with message** — use [`ResultExt::or_raise()`]://! ```rust,ignore//! // BEFORE://! #[error("failed to parse config")]//! Config(#[source] config::Error),//! // → Err(Error::Config(err))//!//! // AFTER://! // → config_call().or_raise(|| message("failed to parse config"))?//! ```//!//! **Guard / assertion** — use [`ensure!`]://! ```rust,ignore//! // BEFORE://! if !condition {//!     return Err(Error::SomethingFailed);//! }//!//! // AFTER (returning ValidationError)://! ensure!(condition, ValidationError::new("something went wrong"));//!//! // AFTER (returning Exn<Message>)://! ensure!(condition, message("something went wrong"));//! ```//!//! ## Updating the function signature//!//! Change the return type, and add the necessary imports://! ```rust,ignore//! // BEFORE://! fn parse(input: &str) -> Result<Value, Error> { ... }//!//! // AFTER (no callee errors wrapped)://! fn parse(input: &str) -> Result<Value, Message> { ... }//!//! // AFTER (callee errors wrapped)://! use gix_error::{message, ErrorExt, Exn, Message, ResultExt};//! fn parse(input: &str) -> Result<Value, Exn<Message>> { ... }//! ```//!//! ## Updating tests//!//! Pattern-matching on enum variants can be replaced with string assertions://! ```rust,ignore//! // BEFORE://! assert!(matches!(result.unwrap_err(), Error::SomethingFailed));//!//! // AFTER://! assert_eq!(result.unwrap_err().to_string(), "something went wrong");//! ```//!//! To access error-specific metadata (e.g. the `input` field on [`ValidationError`]),//! use [`Exn::downcast_any_ref()`] to find a specific error type within the error tree://! ```rust,ignore//! // BEFORE://! match result.unwrap_err() {//!     Error::InvalidInput { input } => assert_eq!(input, "bad"),//!     other => panic!("unexpected: {other}"),//! }//!//! // AFTER://! let err = result.unwrap_err();//! let ve = err.downcast_any_ref::<ValidationError>().expect("is a ValidationError");//! assert_eq!(ve.input.as_deref(), Some("bad".into()));//! ```//!//! # Common Pitfalls//!//! ## Don't use `.erased()` to change the `Exn` type parameter//!//! [`Exn::raise()`] already nests the current `Exn<E>` as a child of a new `Exn<T>`,//! so there is no need to erase the type first. Use [`ErrorExt::and_raise()`] as shorthand://! ```rust,ignore//! // WRONG — double-boxes and discards type information://! io_err.raise().erased().raise(message("context"))//!//! // OK — raise() nests the Exn<io::Error> as a child of Exn<Message> directly://! io_err.raise().raise(message("context"))//!//! // BEST — and_raise() is a shorthand for .raise().raise()://! io_err.and_raise(message("context"))//! ```//!//! Only use [`.erased()`](Exn::erased) when you genuinely need a type-erased `Exn` (no type parameter),//! e.g. to return different error types from the same function via `Result<T, Exn>`.//!//! ## Don't use `.raise_all()` with a single error//!//! [`Exn::raise_all()`] is meant for creating error trees with *multiple* causes.//! If you only have a single causing error, use [`.or_raise()`](ResultExt::or_raise) instead://! ```rust,ignore//! // WRONG — raise_all() is for multiple causes, not a single one://! result.map_err(|e| message("context").raise_all(Some(e.raise())))?;//!//! // RIGHT — or_raise() wraps the error with context directly://! result.or_raise(|| message("context"))?;//! ```//!//! ## Convert `Exn` to [`Error`] at public API boundaries//!//! Porcelain crates (like `gix`) should **not** expose [`Exn<Message>`](Exn) in their public API//! because it does not itself implement [`std::error::Error`].//!//! Instead, convert to [`Error`] (which does implement `std::error::Error`) at the boundary.//! [`Exn`] also converts directly into `Box<dyn std::error::Error + Send + Sync>`, so `?` works//! without an explicit conversion when that is the receiving result's error type://! ```rust,ignore//! // In the porcelain crate's error module://! pub type Error = gix_error::Error;  // not gix_archive::Error (which is Exn<Message>)//!//! // The conversion happens automatically via From<Exn<E>> for Error,//! // so `?` works without explicit .into_error() calls.//! ```//!//! # Feature Flags#![cfg_attr(    all(doc, feature = "document-features"),    doc = ::document_features::document_features!())]//! # Why not `anyhow`?//!//! `anyhow` is a proven and optimized library, and it would certainly suffice for an error-chain based approach//! where users are expected to downcast to concrete types.//!//! What's missing though is `track-caller` which will always capture the location of error instantiation, along with//! compatibility for error trees, which are happening when multiple calls are in flight during concurrency.//!//! Both libraries share the shortcoming of not being able to implement `std::error::Error` on their error type,//! and both provide workarounds.//!//! `exn` is much less optimized, but also costs only a `Box` on the stack,//! which in any case is a step up from `thiserror` which exposed a lot of heft to the stack.#![deny(missing_docs, unsafe_code)]/// A result type to hide the [Exn] error wrapper.mod exn; pub use bstr;pub use exn::{BoxedResultExt, ErrorExt, Exn, Frame, OptionExt, ResultExt, Something, Untyped}; /// An error type that wraps an inner type-erased boxed `std::error::Error` or an `Exn` frame.////// In that, it's similar to `anyhow`, but with support for tracking the call site and trees of errors.////// # Warning: `source()` information is stringified and type-erased////// All `source()` values when created with [`Error::from_error()`] are turned into frames,/// but lose their type information completely. An existing `Error` is retained as a nested error instead./// This is because they are only seen as reference and thus can't be stored.////// # The `auto-chain-error` feature////// If it's enabled, this type is merely a wrapper around [`ChainedError`]. This happens automatically/// so applications that require this don't have to go through an extra conversion.////// When both the `tree-error` and `auto-chain-error` features are enabled, the `tree-error`/// behavior takes precedence and this type uses the tree-based representation.pub struct Error {    #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]    inner: error::Inner,    #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]    inner: ChainedError,} fn root_error_eq(mut error: &(dyn std::error::Error + 'static), other: &str) -> bool {    while let Some(nested) = error.downcast_ref::<Error>() {        error = nested.error();    }    error.to_string() == other} impl PartialEq<str> for Error {    fn eq(&self, other: &str) -> bool {        root_error_eq(self.error(), other)    }} impl PartialEq<&str> for Error {    fn eq(&self, other: &&str) -> bool {        <Self as PartialEq<str>>::eq(self, other)    }} impl PartialEq<String> for Error {    fn eq(&self, other: &String) -> bool {        <Self as PartialEq<str>>::eq(self, other)    }} /// A Result type that uses the [`Error`] type.pub type Result<T = ()> = std::result::Result<T, Error>; mod test;pub use test::{TestError, TestResult}; mod error;pub use error::{Class, Classification, DisplaySource, can_retry, can_retry_lenient}; /// Various kinds of concrete errors that implement [`std::error::Error`].mod concrete;pub use concrete::chain::ChainedError;pub use concrete::classify::{    CorruptionError, NotFoundError, ResourceExhaustionError, ResourceExhaustionKind, RetryableError,};pub use concrete::message::{Message, message};pub use concrete::validate::ValidationError; pub(crate) fn write_location(f: &mut std::fmt::Formatter<'_>, location: &std::panic::Location) -> std::fmt::Result {    write!(f, ", at {}:{}", location.file(), location.line())}