Skip to main content

gix_error/
error.rs

1use crate::Metadata;
2
3// Keep inherent methods on Error and Exn while sharing their implementation and documentation.
4macro_rules! classification_predicates {
5    () => {
6        /// Return the highest-precedence class found anywhere in the error tree.
7        ///
8        /// This selects the smallest [`crate::Class`] according to its recovery precedence, independently
9        /// of traversal order. Native sources and nested [`crate::Error`] values are inspected too.
10        /// Return `None` if no known classification is found. Other classes remain observable through
11        /// [`Self::classify()`]; choosing a dominant class does not make their recovery needs ignorable.
12        pub fn dominant_class(&self) -> Option<Class> {
13            self.classify().dominant_class()
14        }
15
16        /// Return `true` if any stored error or native source has an explicit [`crate::Class::Retryable`] classification.
17        ///
18        /// [`crate::Message`] and [`crate::ClassificationMarker`] can supply this classification.
19        /// Nested [`crate::Error`] values are inspected recursively. Unlike [`Self::can_retry()`], this does not infer
20        /// retryability from I/O error kinds.
21        pub fn is_retryable(&self) -> bool {
22            self.classify().is_retryable()
23        }
24
25        /// Return `true` if any stored error or native source reports resource exhaustion.
26        ///
27        /// This recognizes messages or markers with
28        /// [`crate::Class::ResourceExhaustion`], [`std::collections::TryReserveError`], and
29        /// [`std::io::ErrorKind::OutOfMemory`], including within nested
30        /// [`crate::Error`] values.
31        pub fn is_resource_exhausted(&self) -> bool {
32            self.classify().is_resource_exhausted()
33        }
34
35        /// Return `true` if any stored error, or an error in its [`source()`](std::error::Error::source) chain, is:
36        ///
37        /// * classified as [`crate::Class::Retryable`], or
38        /// * a [`std::io::Error`] with kind `Interrupted` or `TimedOut`.
39        ///
40        /// Nested [`crate::Error`] values are inspected recursively. `false` only means that no known retryable error was
41        /// found; it does not guarantee that retrying cannot succeed. Explicit [`crate::Class::Cancelled`]
42        /// anywhere in the error tree returns `false`, even alongside retryable causes. A `true` result
43        /// does not establish that repeating side effects is safe.
44        pub fn can_retry(&self) -> bool {
45            self.classify().can_retry()
46        }
47
48        /// Apply [`Self::can_retry()`], also accepting [`std::io::Error`] with kind `UnexpectedEof`, `OutOfMemory`,
49        /// `BrokenPipe`, `AddrInUse`, `ConnectionAborted`, `ConnectionReset`, or `ConnectionRefused`.
50        ///
51        /// This applies a more lenient policy than [`Self::can_retry`]. Nested [`crate::Error`] values are inspected recursively.
52        /// Explicit [`crate::Class::Cancelled`] anywhere in the error tree returns `false`.
53        /// Otherwise `false` only means no known retryable error was found; `true` does not establish retry safety.
54        pub fn can_retry_lenient(&self) -> bool {
55            self.classify().can_retry_lenient()
56        }
57
58        /// Return `true` if malformed or internally inconsistent data caused the failure.
59        pub fn is_corrupted(&self) -> bool {
60            self.classify().is_corrupted()
61        }
62
63        /// Return `true` if a requested resource was not found.
64        pub fn is_not_found(&self) -> bool {
65            self.classify().is_not_found()
66        }
67
68        /// The caller requested cancellation; stop rather than retry.
69        pub fn is_cancelled(&self) -> bool {
70            self.classify().is_cancelled()
71        }
72
73        /// Authorization or permissions are insufficient; obtain authorization or change permissions.
74        pub fn is_permission_denied(&self) -> bool {
75            self.classify().is_permission_denied()
76        }
77
78        /// Credentials are missing or rejected; obtain or refresh credentials.
79        pub fn is_unauthenticated(&self) -> bool {
80            self.classify().is_unauthenticated()
81        }
82
83        /// Current state conflicts with the operation; refresh or reconcile state before retrying.
84        pub fn is_conflict(&self) -> bool {
85            self.classify().is_conflict()
86        }
87
88        /// A required capability is unsupported; switch implementation, format, protocol, or strategy.
89        pub fn is_unsupported(&self) -> bool {
90            self.classify().is_unsupported()
91        }
92
93        /// Return `true` if invalid input caused the failure.
94        pub fn is_validation(&self) -> bool {
95            self.classify().is_validation()
96        }
97    };
98}
99
100/// A borrowed error together with its optional caller location, intended for diagnostic display.
101///
102/// Errors owned by a [`crate::exn::Frame`] have the location captured when that frame was created. The first real source
103/// beneath transparent classification markers inherits their frame's location. Other native
104/// [`std::error::Error::source()`] values have no location because no caller location was captured for them.
105///
106/// Unlike [`crate::exn::Frame`], this type neither owns the error nor represents relationships in an error tree. This lets
107/// [`crate::Error::iter_errors_with_locations()`] provide the same lightweight view for the tree-backed and flattened-chain
108/// representations.
109///
110/// Its normal [`Display`](std::fmt::Display) output appends the location when one is available and the
111/// `error-print-location` feature is enabled. Alternate formatting (`{source:#}`) forwards alternate formatting to
112/// the underlying error and always omits the location.
113#[derive(Clone, Copy, Debug)]
114pub struct DisplaySource<'a> {
115    error: &'a (dyn std::error::Error + 'static),
116    location: Option<&'static std::panic::Location<'static>>,
117}
118
119impl<'a> DisplaySource<'a> {
120    /// Return the stored error, preserving its concrete type for downcasting.
121    pub fn error(&self) -> &'a (dyn std::error::Error + 'static) {
122        self.error
123    }
124
125    /// Return the captured or inherited caller location, or `None` for an ordinary native error source.
126    ///
127    /// The file path is the compiler-provided path, before diagnostic formatting shortens it.
128    pub fn location(&self) -> Option<&'static std::panic::Location<'static>> {
129        self.location
130    }
131}
132
133impl std::fmt::Display for DisplaySource<'_> {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        std::fmt::Display::fmt(self.error, f)?;
136        if !f.alternate()
137            && let Some(location) = self.location
138        {
139            crate::write_location(f, location)?;
140        }
141        Ok(())
142    }
143}
144
145impl crate::Error {
146    /// Recover the exception tree for internal processing, preserving its errors, causes, and caller locations.
147    ///
148    /// The tree representation reuses its frame allocation.
149    /// This also reconstructs explicitly raised frames when `auto-chain-error` is enabled. Native sources remain
150    /// owned by their errors and do not become child frames. Use [`crate::Exn::into_error()`] to return to a public boundary.
151    pub fn into_exn(self) -> crate::Exn {
152        #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
153        {
154            crate::Exn::from_boxed_frame(self.into_frame())
155        }
156        #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
157        {
158            crate::Exn::from_chain(self.inner)
159        }
160    }
161
162    /// Lazily visit stored errors and native sources in logical breadth-first order, expanding nested [`crate::Error`] values.
163    ///
164    /// The stored error is first unless it is a classification marker. A frame's native source precedes its explicitly
165    /// raised children. Concrete error types remain available for downcasting, except for classification markers,
166    /// which are always transparent to traversal.
167    /// Use [`Self::classify()`] to inspect classifications.
168    pub fn iter_errors(&self) -> impl Iterator<Item = &(dyn std::error::Error + 'static)> + '_ {
169        self.iter_errors_with_locations().map(|source| source.error)
170    }
171
172    /// Visit the same errors as [`Self::iter_errors()`], with caller locations for explicitly raised frames.
173    /// The first real source beneath transparent classification markers inherits their frame's location; other native
174    /// sources have no caller location of their own. [`DisplaySource`] can render either representation.
175    pub fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ {
176        Errors::new(self.iter_root())
177            .map(Node::display)
178            .filter(|source| !is_transparent_marker(source.error))
179    }
180
181    /// Find the first diagnostic error that downcasts to `T` in logical breadth-first order.
182    /// Classification markers are omitted, as in [`Self::iter_errors()`].
183    pub fn downcast_any_ref<T: std::error::Error + 'static>(&self) -> Option<&T> {
184        self.iter_errors().find_map(|error| error.downcast_ref())
185    }
186
187    /// Follow the unique causal path to a leaf or aggregate, as in [`crate::exn::Frame::probable_cause()`].
188    ///
189    /// Classification markers are always transparent to selection. Nested error graphs and explicitly raised children
190    /// both participate, so a selected boundary at a branch is not replaced by one of its nested causes.
191    /// If selection stays at the root, return the stored error, including a classification-only root.
192    pub fn probable_cause(&self) -> &(dyn std::error::Error + 'static) {
193        self.iter_root().probable_cause().unwrap_or_else(|| self.error())
194    }
195
196    /// Visit the non-empty [`Metadata`] dictionaries of [`crate::Message`] contexts in error traversal order.
197    /// Dictionaries remain separate; use [`Self::metadata_merged()`] to combine them.
198    /// Functions returning metadata document the keys in each context.
199    ///
200    /// To match a class and values on the same message, use [`Self::classify()`] and
201    /// [`Classification::error()`](crate::types::Classification::error) instead of combining independent classification
202    /// and metadata searches.
203    pub fn metadata(&self) -> impl Iterator<Item = &Metadata> + '_ {
204        self.iter_errors()
205            .filter_map(|error| error.downcast_ref::<crate::Message>())
206            .map(|error| &error.values)
207            .filter(|values| !values.is_empty())
208    }
209
210    /// Clone all [`Self::metadata()`] dictionaries into one owned dictionary.
211    ///
212    /// Later values in logical breadth-first error traversal order replace earlier values with the same key.
213    /// Thus, more specific causes override their enclosing contexts. For independent causes, the later-visited
214    /// cause wins; merging does not retain which context supplied a value. Use [`Self::metadata()`] instead when
215    /// that distinction matters. An error without metadata yields an empty dictionary.
216    pub fn metadata_merged(&self) -> Metadata {
217        let mut merged = Metadata::new();
218        for values in self.metadata() {
219            merged.extend(values.iter().map(|(key, value)| (key.clone(), value.clone())));
220        }
221        merged
222    }
223
224    /// Return all known classifications in the same logical breadth-first order as [`Self::iter_errors()`].
225    ///
226    /// Unknown errors are omitted. Classifications aren't deduplicated because distinct errors may independently have
227    /// the same meaning. Each item retains the classified error for downcasting and origin inspection.
228    pub fn classify(&self) -> Classifications<'_> {
229        classify(self)
230    }
231
232    classification_predicates!();
233}
234
235/// Classification helpers for inspecting an exception without consuming it or losing its typed outer error.
236///
237/// The corresponding helpers on [`crate::Error`] would require consuming the exception with
238/// [`into_error()`](crate::Exn::into_error), while dereferencing an exception only exposes its outer error `E`, not
239/// the full error tree. These helpers inspect that tree directly, so callers can recognize a failure's meaning
240/// even when it is wrapped in context, and still propagate the original exception afterward.
241impl<E: std::error::Error + Send + Sync + 'static> crate::Exn<E> {
242    /// Return all known classifications in logical breadth-first order, including native sources and nested
243    /// [`crate::Error`] values.
244    ///
245    /// As with [`crate::Error::classify()`], unknown errors are omitted, classifications aren't deduplicated, and each
246    /// item retains the classified error for downcasting and origin inspection.
247    pub fn classify(&self) -> Classifications<'_> {
248        Classifications(Errors::new(Node::Frame(self.frame().source_frame())))
249    }
250
251    classification_predicates!();
252}
253
254/// The recovery approach suggested by an error.
255///
256/// Variants are listed in suggested recovery precedence, highest first; smaller values have higher precedence
257/// under [`Ord`]. [`crate::Error::dominant_class()`] selects the smallest class present in the error tree.
258/// When multiple classes are present, check earlier variants before later ones: cancellation means stop,
259/// while retryability alone does not override a failure that needs another remedy. Adapt this precedence
260/// to the operation and its concrete errors.
261/// Classification iterators retain error traversal order, not this precedence, and class predicates report
262/// presence independently rather than suppressing lower-priority classes.
263///
264/// A class guides recovery, but does not establish that it is safe. Inspect concrete errors and
265/// partial outcomes before repeating operations with side effects; see [recovery](crate#classification-and-recovery).
266#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
267#[non_exhaustive]
268pub enum Class {
269    /// The caller requested cancellation; stop rather than retry.
270    Cancelled,
271    /// Stored or streamed data was malformed or internally inconsistent.
272    ///
273    /// Recovery may require repairing, replacing, or re-fetching the data, rather than correcting the caller's input.
274    Corruption,
275    /// A finite resource was exhausted.
276    ///
277    /// Recovery may require reducing resource use or making more capacity available before retrying.
278    /// The kind distinguishes an application-configured allocation limit from an allocation failure,
279    /// so callers can choose whether to adjust a limit or address the allocation itself.
280    ResourceExhaustion(crate::ResourceExhaustionKind),
281    /// Function or method input was invalid.
282    ///
283    /// Recovery requires correcting the input rather than retrying the same request unchanged.
284    Validation,
285    /// A required capability is unsupported; switch implementation, format, protocol, or strategy.
286    Unsupported,
287    /// Credentials are missing or rejected; obtain or refresh credentials.
288    Unauthenticated,
289    /// Authorization or permissions are insufficient; obtain authorization or change permissions.
290    PermissionDenied,
291    /// Current state conflicts with the operation; refresh or reconcile state before retrying.
292    Conflict,
293    /// A requested resource does not exist.
294    ///
295    /// Callers may recover by creating the resource, using a fallback, or treating absence as an expected outcome.
296    /// This distinguishes absence from failures that prevent determining whether the resource exists.
297    NotFound,
298    /// Retrying the operation may succeed.
299    ///
300    /// Callers may recover with a bounded retry, possibly after waiting, without changing the request.
301    /// This is not a guarantee of success or a statement that repeating an operation with side effects is safe.
302    Retryable,
303}
304
305/// A semantic class together with the concrete error which established it.
306#[derive(Clone, Copy, Debug)]
307pub struct Classification<'a> {
308    class: Class,
309    error: &'a (dyn std::error::Error + 'static),
310}
311
312/// Lazily inspect the classifications of any borrowed error, including its native sources, I/O payloads and nested
313/// [`crate::Error`] values. Unknown errors are omitted and distinct causes may yield the same classification.
314///
315/// ```
316/// let error = std::io::Error::other(gix_error::not_found("missing object"));
317/// assert!(gix_error::classify(&error).is_not_found());
318/// ```
319pub fn classify<'a>(err: &'a (dyn std::error::Error + 'static)) -> Classifications<'a> {
320    Classifications(Errors::new(Node::boundary(err).unwrap_or(Node::Source {
321        error: err,
322        location: None,
323        source_owner: None,
324    })))
325}
326
327/// A lazy iterator over classified causes. Its predicates consume the remaining iterator.
328/// Class predicates stop at the first match; retry policies inspect all remaining causes so cancellation takes precedence.
329/// Retry predicates also inspect remaining I/O errors whose kinds do not yield a semantic classification.
330pub struct Classifications<'a>(Errors<'a>);
331
332impl<'a> Iterator for Classifications<'a> {
333    type Item = Classification<'a>;
334
335    fn next(&mut self) -> Option<Self::Item> {
336        self.0.find_map(classify_one)
337    }
338}
339
340impl Classifications<'_> {
341    /// Return the highest-precedence class among the remaining causes.
342    ///
343    /// This consumes the remaining iterator and selects the smallest [`Class`] according to its recovery
344    /// precedence, independently of traversal order. Return `None` if no known classification remains.
345    ///
346    /// ```
347    /// use gix_error::{Class, ErrorExt};
348    ///
349    /// let err = gix_error::retryable("temporary failure")
350    ///     .raise_typed()
351    ///     .chain(gix_error::cancelled("user requested cancellation"));
352    ///
353    /// assert_eq!(
354    ///     err.classify().next().map(|item| item.class()),
355    ///     Some(Class::Retryable),
356    ///     "traversal encounters the outer retryable error first"
357    /// );
358    /// assert_eq!(
359    ///     err.classify().dominant_class(),
360    ///     Some(Class::Cancelled),
361    ///     "cancellation takes precedence regardless of traversal order"
362    /// );
363    /// ```
364    pub fn dominant_class(self) -> Option<Class> {
365        self.map(|classification| classification.class()).min()
366    }
367
368    /// Return whether any remaining cause is explicitly marked as retryable.
369    pub fn is_retryable(self) -> bool {
370        self.has(Class::Retryable)
371    }
372
373    /// Apply the conservative retry policy of [`crate::Error::can_retry()`] to the remaining causes.
374    pub fn can_retry(mut self) -> bool {
375        self.retry_policy(|node| node_can_retry(node).0)
376    }
377
378    /// Apply the broader I/O policy of [`crate::Error::can_retry_lenient()`] to the remaining causes.
379    pub fn can_retry_lenient(mut self) -> bool {
380        self.retry_policy(node_can_retry_lenient)
381    }
382
383    /// Return whether any remaining cause reports a missing resource.
384    pub fn is_not_found(self) -> bool {
385        self.has(Class::NotFound)
386    }
387
388    /// Return whether any remaining cause reports [`Class::Cancelled`].
389    pub fn is_cancelled(self) -> bool {
390        self.has(Class::Cancelled)
391    }
392
393    /// Return whether any remaining cause reports [`Class::PermissionDenied`].
394    pub fn is_permission_denied(self) -> bool {
395        self.has(Class::PermissionDenied)
396    }
397
398    /// Return whether any remaining cause reports [`Class::Unauthenticated`].
399    pub fn is_unauthenticated(self) -> bool {
400        self.has(Class::Unauthenticated)
401    }
402
403    /// Return whether any remaining cause reports [`Class::Conflict`].
404    pub fn is_conflict(self) -> bool {
405        self.has(Class::Conflict)
406    }
407
408    /// Return whether any remaining cause reports [`Class::Unsupported`].
409    pub fn is_unsupported(self) -> bool {
410        self.has(Class::Unsupported)
411    }
412
413    /// Return whether any remaining cause reports invalid input.
414    pub fn is_validation(self) -> bool {
415        self.has(Class::Validation)
416    }
417
418    /// Return whether any remaining cause reports malformed or inconsistent data.
419    pub fn is_corrupted(self) -> bool {
420        self.has(Class::Corruption)
421    }
422
423    /// Return whether any remaining cause reports resource exhaustion.
424    pub fn is_resource_exhausted(mut self) -> bool {
425        self.any(|classification| matches!(classification.class(), Class::ResourceExhaustion(_)))
426    }
427
428    fn retry_policy(&mut self, policy: impl Fn(Node<'_>) -> bool) -> bool {
429        let mut retryable = false;
430        for node in self.0.by_ref() {
431            if classify_one(node).is_some_and(|classification| classification.class() == Class::Cancelled) {
432                return false;
433            }
434            retryable |= policy(node);
435        }
436        retryable
437    }
438
439    /// Return whether any remaining cause has exactly `class`.
440    pub fn has(mut self, class: Class) -> bool {
441        self.any(|classification| classification.class() == class)
442    }
443}
444
445impl<'a> Classification<'a> {
446    /// Return the semantic class.
447    pub fn class(&self) -> Class {
448        self.class
449    }
450
451    /// Return the concrete error which established the classification.
452    ///
453    /// A source-bearing [`crate::ClassificationMarker`] identifies its wrapped error. A class-only marker
454    /// supplied through a native [`source()`](std::error::Error::source) identifies the error that owns it.
455    /// A standalone marker without an identifiable subject retains the marker itself as a fallback.
456    pub fn error(&self) -> &'a (dyn std::error::Error + 'static) {
457        self.error
458    }
459
460    /// Return the original I/O error kind, if the underlying error is an [`std::io::Error`].
461    pub fn io_kind(&self) -> Option<std::io::ErrorKind> {
462        self.error.downcast_ref::<std::io::Error>().map(std::io::Error::kind)
463    }
464}
465
466fn classify_one(node: Node<'_>) -> Option<Classification<'_>> {
467    let mut error = node.display().error;
468    let class = if let Some(marker) = error.downcast_ref::<crate::ClassificationMarker>() {
469        let source_owner = match node {
470            Node::Frame(_) => None,
471            Node::Source { source_owner, .. } => source_owner,
472            #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
473            Node::Chain { source_owner, .. } => source_owner,
474        };
475        error = std::error::Error::source(marker).or(source_owner).unwrap_or(error);
476        marker.class()
477    } else if let Some(error) = error.downcast_ref::<crate::Message>() {
478        error.class?
479    } else if error.is::<std::collections::TryReserveError>() {
480        Class::ResourceExhaustion(crate::ResourceExhaustionKind::AllocationFailure)
481    } else {
482        let error = error.downcast_ref::<std::io::Error>()?;
483        match error.kind() {
484            std::io::ErrorKind::NotFound => Class::NotFound,
485            std::io::ErrorKind::PermissionDenied => {
486                // Some transports retain this legacy I/O kind for credential challenges. Prefer the
487                // payload's explicit authentication remedy without losing the original I/O error.
488                if error
489                    .get_ref()
490                    .is_some_and(|payload| has_explicit_authentication_challenge(payload))
491                {
492                    return None;
493                }
494                Class::PermissionDenied
495            }
496            std::io::ErrorKind::Unsupported => Class::Unsupported,
497            std::io::ErrorKind::OutOfMemory => {
498                Class::ResourceExhaustion(crate::ResourceExhaustionKind::AllocationFailure)
499            }
500            _ => return None,
501        }
502    };
503    Some(Classification { class, error })
504}
505
506fn has_explicit_authentication_challenge(error: &(dyn std::error::Error + 'static)) -> bool {
507    // Do not use classification predicates here: native permission fallbacks would recursively
508    // rescan their payloads. This iterative lookahead only needs explicit classification metadata.
509    classify(error).0.any(|node| {
510        let error = node.display().error;
511        error
512            .downcast_ref::<crate::Message>()
513            .is_some_and(|message| message.class == Some(Class::Unauthenticated))
514            || error
515                .downcast_ref::<crate::ClassificationMarker>()
516                .is_some_and(|marker| marker.class() == Class::Unauthenticated)
517    })
518}
519
520fn node_can_retry(node: Node<'_>) -> (bool, Option<std::io::ErrorKind>) {
521    if classify_one(node).is_some_and(|classification| classification.class() == Class::Retryable) {
522        return (true, None);
523    }
524    let io_kind = node
525        .display()
526        .error
527        .downcast_ref::<std::io::Error>()
528        .map(std::io::Error::kind);
529    (
530        matches!(
531            io_kind,
532            Some(std::io::ErrorKind::Interrupted | std::io::ErrorKind::TimedOut)
533        ),
534        io_kind,
535    )
536}
537
538fn node_can_retry_lenient(node: Node<'_>) -> bool {
539    let (can_retry, io_kind) = node_can_retry(node);
540    can_retry
541        || io_kind.is_some_and(|kind| {
542            use std::io::ErrorKind::*;
543            matches!(
544                kind,
545                UnexpectedEof
546                    | OutOfMemory
547                    | BrokenPipe
548                    | AddrInUse
549                    | ConnectionAborted
550                    | ConnectionReset
551                    | ConnectionRefused
552            )
553        })
554}
555
556#[derive(Clone, Copy)]
557enum Node<'a> {
558    Frame(&'a crate::exn::impls::FrameSource),
559    Source {
560        error: &'a (dyn std::error::Error + 'static),
561        location: Option<&'static std::panic::Location<'static>>,
562        source_owner: Option<&'a (dyn std::error::Error + 'static)>,
563    },
564    #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
565    Chain {
566        node: &'a crate::types::ChainedError,
567        index: usize,
568        cursor: Option<usize>,
569        source_owner: Option<&'a (dyn std::error::Error + 'static)>,
570    },
571}
572
573impl<'a> Node<'a> {
574    fn boundary(error: &'a (dyn std::error::Error + 'static)) -> Option<Self> {
575        if let Some(error) = error.downcast_ref::<crate::Error>() {
576            Some(error.iter_root())
577        } else {
578            error.downcast_ref::<crate::exn::impls::FrameSource>().map(Node::Frame)
579        }
580    }
581
582    fn display(self) -> DisplaySource<'a> {
583        let (error, location) = match self {
584            Node::Frame(frame) => (
585                frame.error() as &(dyn std::error::Error + 'static),
586                Some(frame.location()),
587            ),
588            Node::Source { error, location, .. } => (error, location),
589            #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
590            Node::Chain { node, .. } => (node.err.error(), node.err.has_frame_location().then_some(node.location)),
591        };
592        DisplaySource { error, location }
593    }
594
595    fn children(self) -> std::collections::VecDeque<Node<'a>> {
596        // Cause selection follows a path rather than breadth-first order, so each query needs its own chain cursor.
597        #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
598        let root = match self {
599            Node::Chain {
600                node,
601                index,
602                source_owner,
603                ..
604            } => Node::Chain {
605                node,
606                index,
607                cursor: None,
608                source_owner,
609            },
610            root => root,
611        };
612        #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
613        let root = self;
614        let mut traversal = Errors::new(root);
615        traversal.children(root);
616        traversal.pending
617    }
618
619    fn probable_cause(self) -> Option<&'a (dyn std::error::Error + 'static)> {
620        let mut node = self;
621        // Track traversal, not error addresses: a native source can share its owner's address.
622        let mut cause = None;
623        loop {
624            let mut pending = node.children();
625            let mut only_child = None;
626            while let Some(child) = pending.pop_front() {
627                if is_transparent_marker(child.display().error) {
628                    // Marker frames (including nested boundaries storing markers) are transparent, not dead ends.
629                    pending.extend(child.children());
630                } else if only_child.replace(child).is_some() {
631                    return cause;
632                }
633            }
634            node = match only_child {
635                Some(child) => child,
636                None => return cause,
637            };
638            cause = Some(node.display().error);
639        }
640    }
641}
642
643struct Errors<'a> {
644    root: Option<Node<'a>>,
645    previous: Option<Node<'a>>,
646    pending: std::collections::VecDeque<Node<'a>>,
647    #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
648    chains: Vec<(usize, Option<&'a crate::types::ChainedError>)>,
649}
650
651impl<'a> Errors<'a> {
652    fn new(root: Node<'a>) -> Self {
653        Errors {
654            root: Some(root),
655            previous: None,
656            pending: Default::default(),
657            #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
658            chains: Vec::new(),
659        }
660    }
661
662    fn source(
663        &mut self,
664        error: &'a (dyn std::error::Error + 'static),
665        location: Option<&'static std::panic::Location<'static>>,
666    ) {
667        if let Some(node) = Node::boundary(error) {
668            self.pending.push_back(node);
669        } else if let Some(source) = native_source(error) {
670            self.pending
671                .push_back(source.downcast_ref::<crate::exn::impls::FrameSource>().map_or(
672                    Node::Source {
673                        error: source,
674                        location: location.filter(|_| is_transparent_marker(error)),
675                        source_owner: Some(error),
676                    },
677                    Node::Frame,
678                ));
679        }
680    }
681
682    fn children(&mut self, node: Node<'a>) {
683        match node {
684            Node::Frame(frame) => {
685                self.source(frame.error(), Some(frame.location()));
686                self.pending
687                    .extend(frame.children().iter().map(|frame| Node::Frame(frame.source_frame())));
688            }
689            Node::Source { error, location, .. } => self.source(error, location),
690            #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
691            Node::Chain {
692                node, index, cursor, ..
693            } => {
694                if let Some(error) = node.err.error().downcast_ref::<crate::Error>() {
695                    self.pending.push_back(error.iter_root());
696                }
697                let cursor = match cursor {
698                    Some(cursor) => cursor,
699                    None if node.source.is_none() => return,
700                    None => {
701                        self.chains.push((index + 1, node.source.as_deref()));
702                        self.chains.len() - 1
703                    }
704                };
705                // Flattened parents occur in increasing order. One cursor per boundary streams each child once,
706                // even when other error trees are interleaved at their logical breadth-first positions.
707                let (child_index, next) = &mut self.chains[cursor];
708                // A fresh cursor for cause selection can start among siblings belonging to earlier parents.
709                while let Some(child) = next.filter(|child| child.logical_parent.is_some_and(|parent| parent < index)) {
710                    *child_index += 1;
711                    *next = child.source.as_deref();
712                }
713                while let Some(child) = next.filter(|child| child.logical_parent == Some(index)) {
714                    self.pending.push_back(Node::Chain {
715                        node: child,
716                        index: *child_index,
717                        cursor: Some(cursor),
718                        source_owner: child.err.is_native_source().then(|| node.err.error()),
719                    });
720                    *child_index += 1;
721                    *next = child.source.as_deref();
722                }
723            }
724        }
725    }
726}
727
728impl<'a> Iterator for Errors<'a> {
729    type Item = Node<'a>;
730
731    fn next(&mut self) -> Option<Self::Item> {
732        // Defer expansion until the caller asks for another error, so a match need not inspect any of its causes.
733        if let Some(previous) = self.previous.take() {
734            self.children(previous);
735        }
736        let node = self.root.take().or_else(|| self.pending.pop_front())?;
737        self.previous = Some(node);
738        Some(node)
739    }
740}
741
742impl crate::exn::Frame {
743    pub(crate) fn probable_cause_inner(&self) -> Option<&(dyn std::error::Error + 'static)> {
744        Node::Frame(self.source_frame()).probable_cause()
745    }
746
747    pub(crate) fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ {
748        Errors::new(Node::Frame(self.source_frame()))
749            .map(Node::display)
750            .filter(|source| !is_transparent_marker(source.error))
751    }
752}
753
754#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
755mod _impl {
756    use crate::{Error, Exn};
757    use std::fmt::Formatter;
758
759    /// Utilities
760    impl Error {
761        #[expect(
762            clippy::unnecessary_box_returns,
763            reason = "erasure retains the existing frame allocation"
764        )]
765        pub(crate) fn into_frame(self) -> Box<crate::exn::Frame> {
766            let (Inner::Exn(frame) | Inner::ExnAsError(frame)) = self.inner;
767            frame
768        }
769
770        /// Return the error stored at this error boundary.
771        ///
772        /// This can be a classification marker hidden from [`Self::iter_errors()`], and is distinct from
773        /// [`Self::probable_cause()`].
774        pub fn error(&self) -> &(dyn std::error::Error + 'static) {
775            self.inner.frame().error()
776        }
777
778        pub(super) fn iter_root(&self) -> super::Node<'_> {
779            super::Node::Frame(self.inner.frame().source_frame())
780        }
781    }
782
783    pub(crate) enum Inner {
784        ExnAsError(Box<crate::exn::Frame>),
785        Exn(Box<crate::exn::Frame>),
786    }
787
788    impl Inner {
789        pub(crate) fn frame(&self) -> &crate::exn::Frame {
790            match self {
791                Inner::ExnAsError(f) | Inner::Exn(f) => f,
792            }
793        }
794    }
795
796    impl Error {
797        /// Create a new instance representing the given `error`.
798        #[track_caller]
799        pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
800            Error {
801                inner: Inner::ExnAsError(Exn::new(error).into()),
802            }
803        }
804
805        /// Create a new instance representing an already boxed `error`.
806        #[track_caller]
807        pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
808            Self::from_error(crate::exn::Untyped::from_boxed(error))
809        }
810    }
811
812    impl std::fmt::Display for Error {
813        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
814            match &self.inner {
815                Inner::ExnAsError(err) => std::fmt::Display::fmt(err.error(), f),
816                Inner::Exn(frame) => std::fmt::Display::fmt(frame, f),
817            }
818        }
819    }
820
821    impl std::fmt::Debug for Error {
822        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
823            match &self.inner {
824                Inner::ExnAsError(err) => std::fmt::Debug::fmt(err.error(), f),
825                Inner::Exn(frame) => std::fmt::Debug::fmt(frame, f),
826            }
827        }
828    }
829
830    impl std::error::Error for Error {
831        /// Return the first source of an [Exn] error, or the source of a boxed error.
832        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
833            std::error::Error::source(self.inner.frame().source_frame())
834        }
835    }
836
837    impl<E> From<Exn<E>> for Error
838    where
839        E: std::error::Error + Send + Sync + 'static,
840    {
841        fn from(err: Exn<E>) -> Self {
842            Error {
843                inner: Inner::Exn(err.into()),
844            }
845        }
846    }
847}
848#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
849pub(super) use _impl::Inner;
850
851#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
852mod _impl {
853    use crate::{Error, Exn};
854    use std::fmt::Formatter;
855
856    /// Utilities
857    impl Error {
858        /// Return the error stored at this error boundary.
859        ///
860        /// This can be a classification marker hidden from [`Self::iter_errors()`], and is distinct from
861        /// [`Self::probable_cause()`].
862        pub fn error(&self) -> &(dyn std::error::Error + 'static) {
863            self.inner.err.error()
864        }
865
866        pub(super) fn iter_root(&self) -> super::Node<'_> {
867            super::Node::Chain {
868                node: &self.inner,
869                index: 0,
870                cursor: None,
871                source_owner: None,
872            }
873        }
874    }
875
876    impl Error {
877        /// Create a new instance representing the given `error`.
878        #[track_caller]
879        pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
880            Error {
881                inner: Exn::new(error).into_chain(),
882            }
883        }
884
885        /// Create a new instance representing an already boxed `error`.
886        #[track_caller]
887        pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
888            Self::from_error(crate::exn::Untyped::from_boxed(error))
889        }
890    }
891
892    impl std::fmt::Display for Error {
893        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
894            if f.alternate() {
895                return self.fmt_chain(f, true);
896            }
897            if super::is_transparent_marker(self.error())
898                && let Some(diagnostic) = self.iter_errors_with_locations().next()
899            {
900                return std::fmt::Display::fmt(&diagnostic, f);
901            }
902            std::fmt::Display::fmt(&self.inner, f)
903        }
904    }
905
906    impl std::fmt::Debug for Error {
907        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
908            self.fmt_chain(f, false)
909        }
910    }
911
912    impl Error {
913        pub(crate) fn fmt_chain(&self, f: &mut Formatter<'_>, inline: bool) -> std::fmt::Result {
914            let write_error = |error: super::DisplaySource<'_>, f: &mut Formatter<'_>| -> std::fmt::Result {
915                crate::exn::impls::ErrorMode::Display.fmt(error.error(), f)?;
916                if !inline
917                    && !f.alternate()
918                    && let Some(location) = error.location()
919                {
920                    crate::write_location(f, location)?;
921                }
922                Ok(())
923            };
924            let mut errors = self
925                .iter_errors_with_locations()
926                // Boundary contents are emitted separately by the iterator.
927                .filter(|source| !source.error().is::<Error>());
928            let Some(error) = errors.next() else {
929                return std::fmt::Display::fmt(&self.inner, f);
930            };
931            write_error(error, f)?;
932            for (index, error) in errors.enumerate() {
933                if inline {
934                    write!(f, ": ")?;
935                } else {
936                    if index == 0 {
937                        write!(f, "\n\nCaused by:")?;
938                    }
939                    write!(f, "\n    {index}: ")?;
940                }
941                write_error(error, f)?;
942            }
943            Ok(())
944        }
945    }
946
947    impl std::error::Error for Error {
948        /// Return the first source of an [Exn] error, or the source of a boxed error.
949        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
950            self.inner.source()
951        }
952    }
953
954    impl<E> From<Exn<E>> for Error
955    where
956        E: std::error::Error + Send + Sync + 'static,
957    {
958        fn from(err: Exn<E>) -> Self {
959            Error {
960                inner: err.into_chain(),
961            }
962        }
963    }
964}
965
966impl From<crate::Message> for crate::Error {
967    /// Raise the message at the caller's location, including when converted with `.into()` or `?`.
968    /// When used as a function pointer, caller tracking stops at the pointer's invocation shim;
969    /// use a closure such as `|message| message.raise()` to capture a location in the calling code.
970    #[track_caller]
971    fn from(err: crate::Message) -> Self {
972        crate::Exn::new(err).into()
973    }
974}
975
976/// Retain I/O payloads, which `std::io::Error::source()` skips even when they carry a classification or an error tree.
977pub(crate) fn native_source<'a>(
978    err: &'a (dyn std::error::Error + 'static),
979) -> Option<&'a (dyn std::error::Error + 'static)> {
980    match err.downcast_ref::<std::io::Error>() {
981        Some(err) => err.get_ref().map(|err| err as _),
982        None => err.source(),
983    }
984}
985
986pub(crate) fn is_transparent_marker(mut error: &(dyn std::error::Error + 'static)) -> bool {
987    while let Some(nested) = error.downcast_ref::<crate::Error>() {
988        error = nested.error();
989    }
990    error.is::<crate::ClassificationMarker>()
991}