gix_error/lib.rs
1//! Common error types and utilities for error handling.
2//!
3//! # Classification and recovery
4//!
5//! Error classification matters because it lets a generic caller **choose a recovery approach**,
6//! supporting resilient software.
7//! Classes describe different remedies, not just different wording:
8//!
9//! | [`Class`] | Recovery approach |
10//! | --- | --- |
11//! | [`Cancelled`](Class::Cancelled) | Stop; preserve the caller's intent instead of automatically retrying. |
12//! | [`Corruption`](Class::Corruption) | Repair, replace, or re-fetch malformed data. |
13//! | [`ResourceExhaustion`](Class::ResourceExhaustion) | Reduce usage, adjust a limit, or free capacity. |
14//! | [`Validation`](Class::Validation) | Correct the caller's input. |
15//! | [`Unsupported`](Class::Unsupported) | Switch implementation, capability, format, protocol, or strategy. |
16//! | [`Unauthenticated`](Class::Unauthenticated) | Obtain or refresh credentials, distinct from authenticated-but-forbidden. |
17//! | [`PermissionDenied`](Class::PermissionDenied) | Obtain authorization or change permissions. |
18//! | [`Conflict`](Class::Conflict) | Refresh or reconcile state before retrying against the new state. |
19//! | [`NotFound`](Class::NotFound) | Create the resource, use a fallback, or accept absence. |
20//! | [`Retryable`](Class::Retryable) | Consider a bounded retry, possibly after waiting. |
21//!
22//! The table follows [`Class`]'s suggested recovery precedence, highest first. [`Error::dominant_class()`]
23//! and [`Exn::dominant_class()`] return the highest-precedence class present, or `None` for unclassified errors.
24//! For borrowed errors, use [`classify()`] followed by [`dominant_class()`](types::Classifications::dominant_class).
25//! This uses the smallest [`Class`] under its ordering, rather than the first classification in traversal order.
26//!
27//! **A class guides recovery; it does not establish that recovery is safe.** An operation whose outcome is
28//! unknown may already have performed side effects, so retrying can duplicate them even after a transient failure.
29//! Inspect concrete recovery errors and partial outcomes when necessary. Multiple causes can suggest different
30//! remedies; a matching class does not make sibling failures ignorable. Both retry policies reject explicit
31//! cancellation anywhere in the inspected causes; [`Error::is_retryable()`] only reports whether a retryable cause exists.
32//!
33//! Leave errors unclassified when the remedy is unknown. An external program's failed exit status alone does not
34//! establish a class. Preserve genuine callee errors rather than assigning their context an unsupported guess.
35//! Classify intrinsic conditions at their definition; use [`tag()`] for context-dependent classifications.
36//!
37//! # Usage
38//!
39//! Use [`Result`] and [`Error`] for public APIs with erased or message-based errors in plumbing crates and in `gix`.
40//! Public traits, callbacks, iterator items, associated errors, and re-exported APIs follow the same rule.
41//! It's Ok to preserve concrete error types already exposed by public signatures, including
42//! [`ExnResult<T, Specific>`](ExnResult) and [`Exn<Specific>`](Exn), if this helps readability and consumers.
43//! Native `std::io::Result`, standalone concrete-error results, and generic error adapters can retain their types.
44//!
45//! Private and `pub(crate)` implementations also use [`Result`] for erased or message-based errors.
46//! Retain concrete errors when callers benefit from typed recovery or payload access. Use [`ExnResult<T, E>`](ExnResult)
47//! or [`ExnMessageResult<T>`](ExnMessageResult) when callers need a specific exception type or manipulate exception trees.
48//!
49//! The default helpers return [`Error`] and [`Result`]; their `_typed` variants retain an [`Exn<E>`](Exn):
50//!
51//! | Default helper | Typed exception helper |
52//! |---------------|------------------------|
53//! | [`ErrorExt::raise()`] | [`ErrorExt::raise_typed()`] |
54//! | [`ErrorExt::and_raise()`] | [`ErrorExt::and_raise_typed()`] |
55//! | [`ResultExt::or_raise()`] | [`ResultExt::or_raise_typed()`] |
56//! | [`OptionExt::ok_or_raise()`] | [`OptionExt::ok_or_raise_typed()`] |
57//!
58//! Use [`ResultExt::or_error()`] to convert native errors or exceptions to [`Result`] without adding context.
59//! Existing [`Error`] values pass through unchanged. Conversions preserve concrete recovery errors, causes,
60//! metadata, and caller locations; `?`, `.into()`, and [`Exn::into_error()`] also convert exceptions to [`Error`].
61//! Use [`Error::into_exn()`] to recover an exception tree for internal processing, including rearranging child frames.
62//!
63//! # Caller locations
64//!
65//! The `error-print-location` feature enables caller locations in diagnostics. Paths with conventional
66//! `src`, `tests`, `examples`, or `benches` directories are shortened to the containing package directory
67//! and source path; Cargo registry package versions are omitted. For example,
68//! `/home/user/.cargo/registry/src/index.crates.io-hash/gix-url-0.39.0/src/parse.rs:349`
69//! is displayed as `gix-url/src/parse.rs:349`. Package-relative source paths are retained;
70//! paths whose package root cannot be inferred from these directory conventions fall back to the filename.
71//!
72//! This only changes formatting: inspected locations retain the compiler-provided file, line, and column.
73//! A location contains no Cargo package metadata, so a directory that differs from the package name cannot
74//! be renamed automatically. Build owners can use rustc's `--remap-path-prefix` to control captured paths
75//! for arbitrary source layouts. Alternate formatting continues to omit locations.
76//!
77//! # Standard Error Types
78//!
79//! Use these types for diagnostic context when recovery does not depend on a specific condition or structured payload.
80//! Otherwise retain a concrete [`Error`](std::error::Error)-implementing type, and use it with
81//! [`ResultExt::or_raise(<StandardErrorType>)`](ResultExt::or_raise) or
82//! [`OptionExt::ok_or_raise(<StandardErrorType>)`](OptionExt::ok_or_raise), or sibling methods.
83//!
84//! All these types implement [`Error`](std::error::Error).
85//!
86//! ## [`Message`] and [`ClassificationMarker`]
87//!
88//! [`Message`] combines a diagnostic message, an optional [`Class`], and named scalar values. Use it
89//! for diagnostic context, or instead of a chain of type-bearing errors when those layers only describe a single
90//! failure. Keep concrete errors when callers need to match a particular condition, even without a payload.
91//! [`not_found()`], [`validation()`], [`corruption()`], [`retryable()`], [`resource_exhaustion()`],
92//! [`allocation_limit()`], [`allocation_failure()`], [`cancelled()`], [`permission_denied()`],
93//! [`unauthenticated()`], [`conflict()`], and [`unsupported()`] construct classified messages.
94//! [`message()`] and [`Message::new()`] start without a class or values. [`Message::with_class()`] and
95//! [`Message::with()`] add them to the same diagnostic. Use [`message!`] for formatting, equivalent to
96//! [`Message::new(format!("…"))`](Message::new) or `format!("…").into()`.
97//! [`Message::corrupted()`] and [`Message::validation()`] are shortcuts for [`Class::Corruption`] and [`Class::Validation`],
98//! respectively: `message!("invalid record at {offset}").corrupted()` or `message!("invalid count: {count}").validation()`.
99//! The other classes have matching builders: [`Message::not_found()`], [`Message::retryable()`],
100//! [`Message::resource_exhaustion()`], [`Message::allocation_limit()`], [`Message::allocation_failure()`],
101//! [`Message::cancelled()`], [`Message::permission_denied()`], [`Message::unauthenticated()`],
102//! [`Message::conflict()`], and [`Message::unsupported()`].
103//! [`Message::corrupted_error()`], [`Message::validation_error()`], [`Message::not_found_error()`], [`Message::retryable_error()`],
104//! [`Message::resource_exhaustion_error()`], [`Message::allocation_limit_error()`], [`Message::allocation_failure_error()`],
105//! [`Message::cancelled_error()`], [`Message::permission_denied_error()`], [`Message::unauthenticated_error()`],
106//! [`Message::conflict_error()`], and [`Message::unsupported_error()`]
107//! combine these builders with [`ErrorExt::raise()`] when an [`Error`] is needed directly.
108//! Prefer [`corruption()`] or [`validation()`] for static messages when more concise.
109//!
110//! Classification does not determine which diagnostic values can be attached. For example,
111//! `corruption("Malformed reference").with_input(bytes)` preserves offending bytes in the same
112//! error that describes their corruption. No extra validation error is needed just to store input.
113//! Use explicit classified constructors or builders: converting a string to [`Message`] does not infer a class
114//! from the function's return type.
115//!
116//! | Type | Diagnostic | Classification | Purpose |
117//! |------|------------|----------------|---------|
118//! | [`Message`] | Visible message and optional values | Optional | Describe a failure without a custom error type |
119//! | [`ClassificationMarker`] | Transparent, no diagnostic of its own | Required | Classify an existing error while preserving its concrete type |
120//!
121//! ```
122//! use gix_error::{ErrorExt, Message, MetadataValue};
123//!
124//! let error = gix_error::not_found("Reference does not exist")
125//! .with("path", std::path::Path::new("HEAD"))
126//! .raise();
127//! assert!(error.is_not_found());
128//! assert!(error.probable_cause().is::<Message>());
129//! assert_eq!(error.metadata().next().expect("lookup details")["path"], MetadataValue::Path("HEAD".into()));
130//! ```
131//!
132//! Callers should add context using information they already possess and document its keys on the function
133//! that returns it. Preserve real callee errors, especially concrete recovery signals and complex results
134//! discovered by the callee, such as partial outcomes:
135//!
136//! ```
137//! use gix_error::{message, ResultExt, Message, MetadataValue};
138//!
139//! let error = Err::<(), _>(std::io::Error::from(std::io::ErrorKind::NotFound))
140//! .or_raise(|| message("Could not read reference").with("path", std::path::Path::new("HEAD")))
141//! .expect_err("the lookup failed");
142//! assert!(error.is_not_found());
143//! assert!(error.probable_cause().is::<std::io::Error>());
144//! let context = error.error().downcast_ref::<Message>().expect("message context");
145//! assert_eq!(context.class, None, "the callee, not the context, supplies the classification");
146//! let values = error.metadata().next().expect("lookup context");
147//! assert_eq!(values["path"], MetadataValue::Path("HEAD".into()));
148//! ```
149//!
150//! [`Exn::metadata()`] and [`Error::metadata()`] yield each message's non-empty [`Metadata`] dictionary in error traversal order.
151//! Each dictionary maps names to [`MetadataValue`]s. Keys are local to their context. Use [`Exn::metadata_merged()`] or
152//! [`Error::metadata_merged()`] to obtain one owned dictionary, with more specific causes overriding enclosing contexts.
153//! Merging independent causes discards their origin; later-visited values win. [`Metadata`] documents common schemas
154//! for input validation and external program runtime failures.
155//! These conventions do not require collecting additional information or imply a classification.
156//! To identify a specific failure without inspecting its values, see
157//! [matching a specific failure](#matching-a-specific-failure).
158//!
159//! # [`Exn<ErrorType>`](Exn) and [`Exn`]
160//!
161//! The [`Exn`] type does not implement [`Error`](std::error::Error) itself, but is able to store causing errors
162//! via [`ResultExt::or_raise_typed()`] (and sibling methods) as well as location information of the creation site.
163//!
164//! Use exceptions in helpers whose callers need a typed context or tree operations such as [`Exn::chain_all()`].
165//! [`Exn::erased`] and [`ExnResult<T>`](ExnResult) let these helpers combine different exception types.
166//! Other helpers use [`Result`], which retains causes and locations without conversions at each caller.
167//! Preserve concrete public exception signatures.
168//!
169//! Propagate existing [`Error`] values directly with `?` when the callee already provides enough context.
170//! Use `.or_raise(|| message("context information"))` or its siblings when added context helps diagnose
171//! the failure, explains the operation's purpose, or identifies user-controlled input such as configuration values.
172//! Keep such context even when failures are rare, then errors serve as in-code explanation and intent.
173//!
174//! # Callback results
175//!
176//! Public and private callbacks with erased or message-based errors use [`Result<T>`](Result); concrete callback errors
177//! retain their types. Add context with
178//! [`ResultExt::or_raise()`] when propagating a callback failure:
179//! ```
180//! use gix_error::{message, Result, ResultExt};
181//!
182//! fn parse_count(input: &str) -> Result<u64> {
183//! input.parse::<u64>().or_raise(|| message("could not parse count"))
184//! }
185//!
186//! pub fn process(callback: impl FnOnce() -> Result<u64>) -> Result<u64> {
187//! callback().or_raise(|| message("callback failed"))
188//! }
189//!
190//! assert_eq!(process(|| parse_count("42"))?, 42);
191//! # Ok::<(), gix_error::Error>(())
192//! ```
193//!
194//! Private callbacks that need exception-tree operations may use [`ExnResult`] or [`ExnMessageResult`].
195//! Use [`ResultExt::or_erased()`] only when such a callback requires an erased exception.
196//!
197//! # [`Error`] — `Exn` with `std::error::Error`
198//!
199//! Since [`Exn`] does not implement [`std::error::Error`], it cannot be used where that trait is required
200//! (e.g. `std::io::Error::other()`, or as a `#[source]` in another error type).
201//! The [`Error`] type bridges this gap: it implements [`std::error::Error`] and converts from any
202//! [`Exn<E>`](Exn) via [`From`], preserving the full error tree and location information.
203//!
204//! ```rust,ignore
205//! // Convert an Exn to something usable as std::error::Error:
206//! let exn: Exn<Message> = message("something failed").raise_typed();
207//! let err: gix_error::Error = exn.into();
208//! let err: gix_error::Error = exn.into_error();
209//!
210//! // Useful where std::error::Error is required:
211//! std::io::Error::other(exn.into_error())
212//! ```
213//!
214//! It can also be created directly from any `std::error::Error` via [`Error::from_error()`], which preserves
215//! native formatting for tree-backed errors. For native-error results, prefer [`ResultExt::or_error()`]
216//! to capture the caller location and use exception formatting.
217//! When a constructor is needed, invoke it in a closure, e.g. `.map_err(|err| Error::from_boxed(err))`.
218//! Passing the constructor directly to an adapter captures a `FnOnce` shim's location instead of the call site.
219//!
220//! # Tests with [`TestResult`]
221//!
222//! Return [`TestResult`] from `#[test]` functions to propagate ordinary errors, [`Exn<E>`](Exn), and [`Error`]
223//! directly with `?`. It defaults to `Result<(), TestError>`; helpers returning a value can use `TestResult<T>`.
224//! Accepted errors must convert into `Box<dyn std::error::Error + Send + Sync + 'static>`.
225//!
226//! When a test returns an error, Rust's test harness prints [`TestError`]'s [`Debug`](std::fmt::Debug) output,
227//! including the complete diagnostic tree or chain. Captured caller locations are printed when the
228//! `error-print-location` feature is enabled; alternate formatting omits them.
229//!
230//! ```rust,test_harness
231//! use gix_error::{message, ResultExt, TestResult};
232//!
233//! #[test]
234//! fn parses_count() -> TestResult {
235//! let expected: usize = "42".parse()?;
236//! let actual = "42".parse::<usize>().or_raise(|| message("could not parse count"))?;
237//! assert_eq!(actual, expected, "context preserves the parsed count");
238//! Ok(())
239//! }
240//! ```
241//!
242//! # Migrating from `thiserror`
243//!
244//! This section describes the mechanical translation from `thiserror` error enums to `gix-error`.
245//! In `Cargo.toml`, replace `thiserror = "<version>"` with `gix-error = { version = "^0.1.0", path = "../gix-error" }`.
246//!
247//! ## Choosing the replacement type
248//!
249//! Use [`Result`] for diagnostic messages, including validation failures without callee errors, in public and private code.
250//! [`Message`] carries an optional class and named scalar values; [`Error`] retains the diagnostic context and causes.
251//! Keep a concrete error type in [`ExnResult`] when recovery requires a specific condition or structured payload.
252//! Public trait associated types also use [`Error`] for message-based errors, including bare [`Message`] errors.
253//! Preserve concrete public exception signatures. Internal results retain [`ExnMessageResult`] or [`ExnResult`] when
254//! callers need a specific exception type or exception-tree operations.
255//! Define at most one operation-specific error type, normally a public, `#[non_exhaustive]` enum named `Error`.
256//! Related methods should share it. Broad [`Class`] values categorize errors; variants
257//! define specific recovery decisions. Preserve genuine callee errors as causes instead of formatting them into text.
258//!
259//! Use the chosen type directly in signatures, importing it under its canonical name where helpful.
260//! Crate-specific and operation-specific forwarding aliases or renamed error exports are unnecessary.
261//! Facades may re-export the canonical types, as `gix` does with `Error`, `Exn`, `Result`, `ExnResult`, and `ExnMessageResult`.
262//! Always import the result aliases directly and use their bare names in signatures.
263//!
264//! ## Translating variants
265//!
266//! Translate variants to messages only when they provide diagnostics without a specific recovery contract.
267//! Use [`.raise()`](ErrorExt::raise) to wrap standalone errors into an [`Error`], and
268//! [`ResultExt::or_raise()`] to preserve callee errors with additional context.
269//! For early returns, use [`bail!`] with a concrete error, message, or format arguments, such as
270//! `bail!(Error::SomethingFailed)`, `bail!(message("something went wrong"))`, or `bail!("invalid input: {input}")`.
271//! Prefer to import `bail` with `use gix_error::bail;` (or `use gix::error::bail;` in applications) and
272//! invoke it as `bail!(...)` instead of using a qualified path.
273//! String shorthand is unclassified unless a builder supplies a class. In plumbing APIs, classify known input or data failures explicitly, e.g.
274//! `bail!(validation("invalid input"))` or `bail!("invalid record at {offset}".corrupted())`.
275//! To add context when returning [`Result`], use `bail!(err.and_raise(message("context")))`;
276//! reserve [`ErrorExt::and_raise_typed()`] for [`ExnResult`]s that require an exception type.
277//! When passing an existing [`Exn`], keep any context and explicit erasure inside the macro;
278//! propagating an existing public [`Error`] should use `return Err(err)`.
279//!
280//! **Static message variant:**
281//! ```rust,ignore
282//! // BEFORE:
283//! #[error("something went wrong")]
284//! SomethingFailed,
285//! // → Err(Error::SomethingFailed)
286//!
287//! // AFTER (returning gix_error::Result):
288//! // → Err(message("something went wrong").raise())
289//! ```
290//!
291//! **Formatted message variant:**
292//! ```rust,ignore
293//! // BEFORE:
294//! #[error("unsupported format '{format:?}'")]
295//! Unsupported { format: Format },
296//! // → Err(Error::Unsupported { format })
297//!
298//! // AFTER (returning gix_error::Result):
299//! // → Err(message!("unsupported format '{format:?}'").raise())
300//! ```
301//!
302//! **`#[from]` / `#[error(transparent)]` variant** without a recovery contract — delete the forwarding variant;
303//! at each call site, use [`ResultExt::or_raise()`] to add context:
304//! ```rust,ignore
305//! // BEFORE:
306//! #[error(transparent)]
307//! Io(#[from] std::io::Error),
308//! // → something_that_returns_io_error()? // auto-converted via From
309//!
310//! // AFTER (the variant is deleted):
311//! // → something_that_returns_io_error()
312//! // .or_raise(|| message("context about what failed"))?
313//! ```
314//!
315//! **`#[source]` variant with diagnostic context only** — use [`ResultExt::or_raise()`]:
316//! ```rust,ignore
317//! // BEFORE:
318//! #[error("failed to parse config")]
319//! Config(#[source] config::Error),
320//! // → Err(Error::Config(err))
321//!
322//! // AFTER:
323//! // → config_call().or_raise(|| message("failed to parse config"))?
324//! ```
325//!
326//! **Guard / assertion** — use [`ensure!`]:
327//! ```rust,ignore
328//! // BEFORE:
329//! if !condition {
330//! return Err(Error::SomethingFailed);
331//! }
332//!
333//! // AFTER (returning Exn<Message>, with a validation class):
334//! ensure!(condition, gix_error::validation("something went wrong"));
335//!
336//! // AFTER (returning Exn<Message>):
337//! ensure!(condition, message("something went wrong"));
338//! ```
339//!
340//! ## Updating the function signature
341//!
342//! When replacing a diagnostic-only error enum, change the return type and add the necessary imports:
343//! ```rust,ignore
344//! // BEFORE:
345//! fn parse(input: &str) -> Result<Value, Error> { ... }
346//!
347//! // AFTER (public or private):
348//! use gix_error::{message, ErrorExt, Result, ResultExt};
349//! fn parse(input: &str) -> Result<Value> { ... }
350//! ```
351//! Public APIs that already expose a concrete error, such as `ExnResult<Value, SpecificError>`, retain that type.
352//!
353//! ## Updating tests
354//!
355//! Tests of diagnostic wording can use string assertions:
356//! ```rust,ignore
357//! assert_eq!(result.expect_err("the operation fails").to_string(), "something went wrong");
358//! ```
359//! Keep variant assertions for recovery contracts, and test structured payloads directly.
360//!
361//! For semantic checks, both [`Exn`] and [`Error`] provide [`is_retryable()`](Exn::is_retryable),
362//! [`is_not_found()`](Exn::is_not_found), [`is_validation()`](Exn::is_validation),
363//! [`is_corrupted()`](Exn::is_corrupted), [`is_resource_exhausted()`](Exn::is_resource_exhausted),
364//! [`is_cancelled()`](Exn::is_cancelled), [`is_permission_denied()`](Exn::is_permission_denied),
365//! [`is_unauthenticated()`](Exn::is_unauthenticated), [`is_conflict()`](Exn::is_conflict), and
366//! [`is_unsupported()`](Exn::is_unsupported).
367//! These inspect causes as well as the outermost error. `is_retryable()` requires an explicit retry classification;
368//! [`Exn::can_retry()`] and [`Error::can_retry()`] additionally recognize certain I/O error kinds.
369//! I/O errors with kind `NotFound`, `OutOfMemory`, `PermissionDenied`, or `Unsupported` receive corresponding
370//! semantic classifications; other kinds remain unclassified. For legacy credential-challenge I/O wrappers,
371//! an explicit [`Class::Unauthenticated`] anywhere in a `PermissionDenied` payload takes precedence over that
372//! wrapper's native permission classification. The original I/O error remains available for downcasting;
373//! independently classified permission causes in its payload remain visible.
374//! `Interrupted` alone does not establish caller cancellation.
375//! Retry predicates also inspect original I/O kinds, but explicit cancellation takes precedence.
376//!
377//! For application-level cancellation, use [`cancelled()`]. Preserve genuine I/O errors as causes.
378//! Classification itself neither clears interruption state nor retries.
379//! ```
380//! use gix_error::ErrorExt;
381//!
382//! let err = gix_error::cancelled("Cancelled by user").raise();
383//! assert!(err.is_cancelled());
384//! assert!(!err.can_retry());
385//! assert!(!err.can_retry_lenient());
386//! ```
387//!
388//! Use [`Exn::probable_cause()`] to inspect the likely root cause. It follows a single causal path, stopping at the
389//! first branch rather than choosing an arbitrary sibling. Classification markers are transparent to this selection.
390//! [`Exn::classify()`] and [`Error::classify()`] expose each known classification together with its original error.
391//! Custom payloads of [`std::io::Error`] are inspected too, including any nested [`Error`] trees.
392//!
393//! [`Message`] supplies its own diagnostic and optional classification. In contrast, [`ClassificationMarker`]
394//! only supplies classification metadata. Prefer defining intrinsic classifications on error types you control:
395//!
396//! * For a leaf error or variant whose classification is part of its meaning, return a constant marker from
397//! [`std::error::Error::source()`]. This makes every construction site carry the classification without repeated
398//! [`tag()`] calls.
399//! * Use [`tag()`] when a classification depends on the calling context, or when you cannot modify the error type.
400//! It preserves the concrete error and its diagnostic.
401//!
402//! For example, a caller may know that an `AlreadyExists` I/O error is retryable in its operation:
403//! ```
404//! use gix_error::{Class, ClassificationMarker, ErrorExt, tag};
405//!
406//! let err = tag(
407//! std::io::Error::from(std::io::ErrorKind::AlreadyExists),
408//! Class::Retryable,
409//! ).raise();
410//! assert!(err.is_retryable());
411//! assert!(err.probable_cause().is::<std::io::Error>());
412//! assert!(err.downcast_any_ref::<ClassificationMarker>().is_none());
413//! let classification = err.classify().next().expect("the tag is first");
414//! assert!(classification.error().is::<std::io::Error>());
415//! assert_eq!(classification.io_kind(), Some(std::io::ErrorKind::AlreadyExists));
416//! ```
417//!
418//! Custom error types preserve classifications by exposing their immediate cause as `Some(inner)` from
419//! [`std::error::Error::source()`]. Forwarding to `inner.source()` instead can hide a classification carried by
420//! `inner` itself. A custom leaf error can borrow a constant such as [`ClassificationMarker::NOT_FOUND`]
421//! as its source to preserve its classification without defining a static or adding a generic category to its diagnostic:
422//! ```
423//! use gix_error::{ClassificationMarker, ErrorExt};
424//!
425//! #[derive(Debug)]
426//! struct MissingObject;
427//!
428//! impl std::fmt::Display for MissingObject {
429//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430//! f.write_str("the requested object is missing from the object database")
431//! }
432//! }
433//!
434//! impl std::error::Error for MissingObject {
435//! fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
436//! Some(const { &ClassificationMarker::NOT_FOUND })
437//! }
438//! }
439//!
440//! let err = MissingObject.raise();
441//! assert!(err.is_not_found());
442//! assert!(err.probable_cause().is::<MissingObject>());
443//! assert!(err.classify().next().expect("a classified owner").error().is::<MissingObject>());
444//! ```
445//! Use classification predicates rather than downcasting to [`Message`] just to recognize
446//! a category: diagnostic iterators and downcasts skip all classification markers. Exception and test reports
447//! omit their wrappers too, while raw [`std::error::Error::source()`] chains retain them. Genuine classified errors
448//! remain causal and can still be downcast to inspect their payloads. When storing an [`Exn`] in a custom error, convert it with
449//! [`Exn::into_error()`] so the source can expose its complete tree.
450//!
451//! Record offending input with [`Message::with_input()`], then inspect the documented [metadata](Exn::metadata()) key:
452//! ```
453//! use gix_error::{ErrorExt, MetadataValue};
454//!
455//! let err = gix_error::validation("invalid input").with_input(b"bad".as_slice()).raise();
456//! let values = err.metadata().find(|values| values.contains_key("input")).expect("input context");
457//! assert_eq!(values["input"], MetadataValue::Bytes(b"bad".as_slice().into()));
458//! ```
459//!
460//! ## Matching a specific failure
461//!
462//! Downcast to the operation's error enum and match a variant when a broad category such as [`Class::NotFound`]
463//! isn't specific enough for recovery. The enum retains structured data and identifies the condition independently
464//! of diagnostic wording. For intrinsic classifications on leaf variants of an enum you define, prefer an exhaustive
465//! match on `self` in [`std::error::Error::source()`], returning a constant marker as below. Each new variant then
466//! requires an explicit classification choice. Wrapping, erasure, and conversion to [`Error`] preserve the
467//! classification, so callers do not need to repeat it with [`tag()`].
468//!
469//! ```
470//! use gix_error::{ErrorExt, message};
471//!
472//! mod merge {
473//! use gix_error::ClassificationMarker;
474//!
475//! #[derive(Debug)]
476//! #[non_exhaustive]
477//! pub enum Error {
478//! MissingBinaryMergeResult,
479//! }
480//!
481//! impl std::fmt::Display for Error {
482//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483//! match self {
484//! Self::MissingBinaryMergeResult => f.write_str("The binary merge result could not be selected"),
485//! }
486//! }
487//! }
488//! impl std::error::Error for Error {
489//! fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
490//! match self {
491//! Self::MissingBinaryMergeResult => Some(const { &ClassificationMarker::NOT_FOUND }),
492//! }
493//! }
494//! }
495//! }
496//!
497//! fn recover(err: gix_error::Error) -> gix_error::Result<()> {
498//! match err.downcast_any_ref::<merge::Error>() {
499//! Some(merge::Error::MissingBinaryMergeResult) => Ok(()), // Apply the caller's fallback.
500//! _ => Err(err), // Preserve unfamiliar variants and all other errors.
501//! }
502//! }
503//!
504//! let err = merge::Error::MissingBinaryMergeResult.and_raise(message("Tree merge failed"));
505//!
506//! assert!(err.is_not_found(), "the variant supplies its intrinsic classification");
507//! assert!(
508//! matches!(err.downcast_any_ref::<merge::Error>(), Some(merge::Error::MissingBinaryMergeResult)),
509//! "the classified variant remains available for recovery"
510//! );
511//! recover(err)?;
512//! # Ok::<(), gix_error::Error>(())
513//! ```
514//!
515//! Downcasting and [`types::Classification::error()`] retain concrete subjects through contexts and [`Error`] conversion.
516//! Matching one cause does not make other failures in an aggregate ignorable.
517//!
518//! # Common Pitfalls
519//!
520//! ## Don't use `.erased()` to change the `Exn` type parameter
521//!
522//! [`Exn::raise()`] already nests the current `Exn<E>` as a child of a new `Exn<T>` without prior erasure.
523//! On native errors, use [`ErrorExt::and_raise()`] for an [`Error`], or
524//! [`ErrorExt::and_raise_typed()`] for a typed exception.
525//!
526//! Use [`.erased()`](Exn::erased) when you need a type-erased `Exn` (no type parameter),
527//! e.g. when combining different failures in an exception tree. Otherwise return [`Result<T>`](Result).
528//!
529//! ## Don't use `.raise_all()` with a single error
530//!
531//! [`Exn::raise_all()`] is meant for creating error trees with *multiple* causes.
532//! If you only have a single causing error, use [`.or_raise()`](ResultExt::or_raise) instead:
533//! ```rust,ignore
534//! // WRONG — raise_all() is for multiple causes, not a single one:
535//! result.map_err(|e| message("context").raise_all(Some(e.raise_typed())))?;
536//!
537//! // RIGHT — or_raise() wraps the error with context directly:
538//! result.or_raise(|| message("context"))?;
539//! ```
540//!
541//! ## Avoid conversions between diagnostic-only helpers
542//!
543//! Use [`Result`] throughout a call chain unless callers need typed exceptions or exception-tree operations.
544//! Helpers can then return their callee's result directly:
545//! ```
546//! use gix_error::{message, Result, ResultExt};
547//!
548//! fn parse_count(input: &str) -> Result<u64> {
549//! input.parse::<u64>().or_raise(|| message("could not parse count"))
550//! }
551//!
552//! pub fn count(input: &str) -> Result<u64> {
553//! parse_count(input)
554//! }
555//! assert_eq!(count("42")?, 42);
556//! # Ok::<(), gix_error::Error>(())
557//! ```
558//! Concrete public exception signatures remain typed. Use [`ResultExt::or_error()`] when a callee returns such
559//! an exception and the caller returns [`Result`]. [`Error`] implements [`std::error::Error`]; [`Exn`] does not.
560//!
561//! # Supporting types
562//!
563//! Frequently used error types, extension traits, result aliases, and constructors are available at the crate root.
564//! Utility types for flattened chains, classification, and diagnostic display live in [`types`]. Exception frames
565//! and the default type-erasure marker live in [`exn`]; [`Exn`] and its extension traits are only exported at the root.
566//!
567//! # Feature Flags
568#![cfg_attr(
569 all(doc, feature = "document-features"),
570 doc = ::document_features::document_features!()
571)]
572//! # Why not `anyhow`?
573//!
574//! `anyhow` is a proven and optimized library, and it would certainly suffice for an error-chain based approach
575//! where users are expected to downcast to concrete types.
576//!
577//! What's missing though is `track-caller` which will always capture the location of error instantiation, along with
578//! compatibility for error trees, which are happening when multiple calls are in flight during concurrency.
579//!
580//! [`Exn`] intentionally does not implement `std::error::Error`; the default helpers return [`Error`], which does.
581//!
582//! `exn` is much less optimized, but also costs only a `Box` on the stack,
583//! which in any case is a step up from `thiserror` which exposed a lot of heft to the stack.
584#![deny(missing_docs, unsafe_code)]
585pub mod exn;
586pub mod types;
587
588#[cfg(feature = "bstr")]
589pub use bstr;
590pub use exn::{
591 ext::{BoxedResultExt, ErrorExt, OptionExt, ResultExt},
592 impls::Exn,
593};
594
595/// An error type that wraps an inner type-erased boxed `std::error::Error` or an `Exn` frame.
596///
597/// In that, it's similar to `anyhow`, but with support for tracking the call site and trees of errors.
598///
599/// # Native error sources
600///
601/// [`Error::from_error()`] retains the concrete error and its native [`source()`](std::error::Error::source) chain.
602/// Use [`Error::downcast_any_ref()`] or [`Error::iter_errors()`] to inspect the original types, including sources
603/// within nested [`Error`] values. This also applies when the `auto-chain-error` feature is enabled.
604///
605/// In tree mode, standard `source()` traversal prefers the stored error's native source; otherwise it follows the
606/// first explicitly raised child. Nonleaf explicit children are exposed through owning source boundaries so traversal
607/// retains their descendants. These boundaries display only their current diagnostic, including with alternate Display,
608/// while explicit leaves and native sources retain their raw concrete payloads. Raw standard-source downcasts can thus
609/// encounter wrappers; use [`Error::downcast_any_ref()`] or [`Error::iter_errors()`] for typed inspection of the complete
610/// tree. Standard traversal follows one path, not every branch; use [`Exn::into_chain()`] for a flattened source chain.
611///
612/// # The `auto-chain-error` feature
613///
614/// If it's enabled, this type is merely a wrapper around [`ChainedError`](types::ChainedError). This happens automatically
615/// so applications that require this don't have to go through an extra conversion.
616///
617/// When both the `tree-error` and `auto-chain-error` features are enabled, the `tree-error`
618/// behavior takes precedence and this type uses the tree-based representation.
619///
620/// With `auto-chain-error`, [`Debug`](std::fmt::Debug) reports the complete diagnostic chain,
621/// so returning [`Result`] from `main()` retains the underlying causes. Caller locations are printed only with the
622/// `error-print-location` feature, including for errors returned from `main()`. Alternate Debug (`{error:#?}`) omits them.
623/// Normal [`Display`](std::fmt::Display) shows the root diagnostic; alternate Display (`{error:#}`) joins the
624/// complete chain with `: ` and omits locations, suitable for single-line error messages.
625pub struct Error {
626 #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
627 inner: error::Inner,
628 #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
629 inner: types::ChainedError,
630}
631
632fn root_error_eq(mut error: &(dyn std::error::Error + 'static), other: &str) -> bool {
633 while let Some(nested) = error.downcast_ref::<Error>() {
634 error = nested.error();
635 }
636 error.to_string() == other
637}
638
639impl PartialEq<str> for Error {
640 fn eq(&self, other: &str) -> bool {
641 root_error_eq(self.error(), other)
642 }
643}
644
645impl PartialEq<&str> for Error {
646 fn eq(&self, other: &&str) -> bool {
647 <Self as PartialEq<str>>::eq(self, other)
648 }
649}
650
651impl PartialEq<String> for Error {
652 fn eq(&self, other: &String) -> bool {
653 <Self as PartialEq<str>>::eq(self, other)
654 }
655}
656
657/// The result type for erased or message-based errors in plumbing and porcelain crates.
658///
659/// Uses [`Error`] and defaults to unit success. Public and private implementations use this alias unless
660/// callers need a concrete error type or exception-tree operations provided by [`ExnResult`] or [`ExnMessageResult`].
661/// Public APIs that already expose concrete exception types retain those types.
662pub type Result<T = ()> = std::result::Result<T, Error>;
663
664/// A result with an [`Exn<E>`](Exn) error, defaulting to unit success and an erased error type.
665///
666/// `ExnResult<T>` uses the same [`exn::Untyped`] marker as bare [`Exn`]. Specify `E` to retain a
667/// concrete error type; [`ExnMessageResult`] is the shorthand for message contexts. Use these aliases when callers
668/// need typed exceptions or exception-tree operations; other public and private code uses [`Result`].
669/// All standard result operations and [`ResultExt`] methods remain available. Use [`ResultExt::or_erased()`] for
670/// callbacks requiring erased exceptions, and `?` to propagate exceptions into [`Error`].
671/// Preserve this alias in public signatures that already expose a concrete error type.
672///
673/// ```
674/// use gix_error::{ErrorExt, ExnResult};
675///
676/// fn parse_count(input: &str) -> ExnResult<u64, std::num::ParseIntError> {
677/// input.parse::<u64>().map_err(ErrorExt::raise_typed)
678/// }
679///
680/// let error = parse_count("not a count").expect_err("the count contains letters");
681/// assert_eq!(
682/// error.error().kind(),
683/// &std::num::IntErrorKind::InvalidDigit,
684/// "callers can inspect the concrete error without downcasting"
685/// );
686/// ```
687pub type ExnResult<T = (), E = exn::Untyped> = std::result::Result<T, Exn<E>>;
688
689/// A result with a [`Message`] exception, defaulting to unit success.
690///
691/// This is [`ExnResult<T, Message>`](ExnResult). Use it when callers need direct access to the [`Message`]
692/// context or exception-tree operations. Other message-based public and private APIs use [`Result<T>`](Result).
693/// Construct it with [`ResultExt::or_raise_typed()`], [`ErrorExt::raise_typed()`], or [`bail!`].
694///
695/// ```
696/// use gix_error::{bail, ExnMessageResult};
697///
698/// fn validate(ready: bool) -> ExnMessageResult {
699/// if !ready {
700/// bail!("not ready");
701/// }
702/// Ok(())
703/// }
704///
705/// validate(true)?;
706/// assert_eq!(validate(false).expect_err("not ready").error().message, "not ready");
707/// # Ok::<(), gix_error::Error>(())
708/// ```
709pub type ExnMessageResult<T = ()> = ExnResult<T, Message>;
710
711mod test;
712pub use test::{TestError, TestResult};
713
714mod error;
715pub use error::{Class, classify};
716
717/// Various kinds of concrete errors that implement [`std::error::Error`].
718mod concrete;
719
720pub use concrete::classify::{ClassificationMarker, ResourceExhaustionKind, tag};
721pub use concrete::message::message;
722pub use concrete::metadata::{
723 Message, Metadata, MetadataValue, allocation_failure, allocation_limit, cancelled, conflict, corruption, not_found,
724 permission_denied, resource_exhaustion, retryable, unauthenticated, unsupported, validation,
725};
726
727mod location;
728pub(crate) use location::write as write_location;