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 `true` if any stored error or native source has an explicit [`crate::Class::Retryable`] classification.
7        ///
8        /// [`crate::Message`] and [`crate::ClassificationMarker`] can supply this classification.
9        /// Nested [`crate::Error`] values are inspected recursively. Unlike [`Self::can_retry()`], this does not infer
10        /// retryability from I/O error kinds.
11        pub fn is_retryable(&self) -> bool {
12            self.classify().is_retryable()
13        }
14
15        /// Return `true` if any stored error or native source reports resource exhaustion.
16        ///
17        /// This recognizes messages or markers with
18        /// [`crate::Class::ResourceExhaustion`], [`std::collections::TryReserveError`], and
19        /// [`std::io::ErrorKind::OutOfMemory`], including within nested
20        /// [`crate::Error`] values.
21        pub fn is_resource_exhausted(&self) -> bool {
22            self.classify().is_resource_exhausted()
23        }
24
25        /// Return `true` if any stored error, or an error in its [`source()`](std::error::Error::source) chain, is:
26        ///
27        /// * classified as [`crate::Class::Retryable`], or
28        /// * classified as [`crate::Class::Io`] with kind `Interrupted` or `TimedOut`.
29        ///
30        /// Nested [`crate::Error`] values are inspected recursively. `false` only means that no known retryable error was
31        /// found; it does not guarantee that retrying cannot succeed.
32        pub fn can_retry(&self) -> bool {
33            self.classify().can_retry()
34        }
35
36        /// Apply [`Self::can_retry()`], also accepting [`std::io::Error`] with kind `UnexpectedEof`, `OutOfMemory`,
37        /// `BrokenPipe`, `AddrInUse`, `ConnectionAborted`, `ConnectionReset`, or `ConnectionRefused`.
38        ///
39        /// This applies a more lenient policy than [`Self::can_retry`]. Nested [`crate::Error`] values are inspected recursively.
40        /// `false` only means that no known retryable error was found; it does not guarantee that retrying cannot succeed.
41        pub fn can_retry_lenient(&self) -> bool {
42            self.classify().can_retry_lenient()
43        }
44
45        /// Return `true` if malformed or internally inconsistent data caused the failure.
46        pub fn is_corrupted(&self) -> bool {
47            self.classify().is_corrupted()
48        }
49
50        /// Return `true` if a requested resource was not found.
51        pub fn is_not_found(&self) -> bool {
52            self.classify().is_not_found()
53        }
54
55        /// Return `true` if invalid input caused the failure.
56        pub fn is_validation(&self) -> bool {
57            self.classify().is_validation()
58        }
59    };
60}
61
62/// A borrowed error together with its optional caller location, intended for diagnostic display.
63///
64/// Errors owned by a [`crate::exn::Frame`] have the location captured when that frame was created. The first real source
65/// beneath transparent classification markers inherits their frame's location. Other native
66/// [`std::error::Error::source()`] values have no location because no caller location was captured for them.
67///
68/// Unlike [`crate::exn::Frame`], this type neither owns the error nor represents relationships in an error tree. This lets
69/// [`crate::Error::iter_errors_with_locations()`] provide the same lightweight view for the tree-backed and flattened-chain
70/// representations.
71///
72/// Its normal [`Display`](std::fmt::Display) output appends the location when one is available. Alternate formatting
73/// (`{source:#}`) forwards alternate formatting to the underlying error and always omits the location.
74#[derive(Clone, Copy, Debug)]
75pub struct DisplaySource<'a> {
76    error: &'a (dyn std::error::Error + 'static),
77    location: Option<&'static std::panic::Location<'static>>,
78}
79
80impl<'a> DisplaySource<'a> {
81    /// Return the stored error, preserving its concrete type for downcasting.
82    pub fn error(&self) -> &'a (dyn std::error::Error + 'static) {
83        self.error
84    }
85
86    /// Return the captured or inherited caller location, or `None` for an ordinary native error source.
87    pub fn location(&self) -> Option<&'static std::panic::Location<'static>> {
88        self.location
89    }
90}
91
92impl std::fmt::Display for DisplaySource<'_> {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        std::fmt::Display::fmt(self.error, f)?;
95        if !f.alternate()
96            && let Some(location) = self.location
97        {
98            crate::write_location(f, location)?;
99        }
100        Ok(())
101    }
102}
103
104impl crate::Error {
105    /// Lazily visit stored errors and native sources in logical breadth-first order, expanding nested [`crate::Error`] values.
106    ///
107    /// The stored error is first unless it is a classification marker. A frame's native source precedes its explicitly
108    /// raised children. Concrete error types remain available for downcasting, except for classification markers,
109    /// which are always transparent to traversal.
110    /// Use [`Self::classify()`] to inspect classifications.
111    pub fn iter_errors(&self) -> impl Iterator<Item = &(dyn std::error::Error + 'static)> + '_ {
112        self.iter_errors_with_locations().map(|source| source.error)
113    }
114
115    /// Visit the same errors as [`Self::iter_errors()`], with caller locations for explicitly raised frames.
116    /// The first real source beneath transparent classification markers inherits their frame's location; other native
117    /// sources have no caller location of their own. [`DisplaySource`] can render either representation.
118    pub fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ {
119        Errors::new(self.iter_root()).filter(|source| !is_transparent_marker(source.error))
120    }
121
122    /// Find the first diagnostic error that downcasts to `T` in logical breadth-first order.
123    /// Classification markers are omitted, as in [`Self::iter_errors()`].
124    pub fn downcast_any_ref<T: std::error::Error + 'static>(&self) -> Option<&T> {
125        self.iter_errors().find_map(|error| error.downcast_ref())
126    }
127
128    /// Follow the unique causal path to a leaf or aggregate, as in [`crate::exn::Frame::probable_cause()`].
129    ///
130    /// Classification markers are always transparent to selection. Nested error graphs and explicitly raised children
131    /// both participate, so a selected boundary at a branch is not replaced by one of its nested causes.
132    /// If selection stays at the root, return the stored error, including a classification-only root.
133    pub fn probable_cause(&self) -> &(dyn std::error::Error + 'static) {
134        self.iter_root().probable_cause().unwrap_or_else(|| self.error())
135    }
136
137    /// Visit the non-empty [`Metadata`] dictionaries of [`crate::Message`] contexts in error traversal order.
138    /// Dictionaries remain separate. Functions returning metadata document the keys in each context.
139    ///
140    /// To match a class and values on the same message, use [`Self::classify()`] and
141    /// [`Classification::error()`](crate::types::Classification::error) instead of combining independent classification
142    /// and metadata searches.
143    pub fn metadata(&self) -> impl Iterator<Item = &Metadata> + '_ {
144        self.iter_errors()
145            .filter_map(|error| error.downcast_ref::<crate::Message>())
146            .map(|error| &error.values)
147            .filter(|values| !values.is_empty())
148    }
149
150    /// Return all known classifications in the same logical breadth-first order as [`Self::iter_errors()`].
151    ///
152    /// Unknown errors are omitted. Classifications aren't deduplicated because distinct errors may independently have
153    /// the same meaning. Each item retains the classified error for downcasting and origin inspection.
154    pub fn classify(&self) -> Classifications<'_> {
155        classify(self)
156    }
157
158    classification_predicates!();
159}
160
161/// Classification helpers for inspecting an exception without consuming it or losing its typed outer error.
162///
163/// The corresponding helpers on [`crate::Error`] would require consuming the exception with
164/// [`into_error()`](crate::Exn::into_error), while dereferencing an exception only exposes its outer error `E`, not
165/// the full error tree. These helpers inspect that tree directly, so callers can recognize a failure's meaning
166/// even when it is wrapped in context, and still propagate the original exception afterward.
167impl<E: std::error::Error + Send + Sync + 'static> crate::Exn<E> {
168    /// Return all known classifications in logical breadth-first order, including native sources and nested
169    /// [`crate::Error`] values.
170    ///
171    /// As with [`crate::Error::classify()`], unknown errors are omitted, classifications aren't deduplicated, and each
172    /// item retains the classified error for downcasting and origin inspection.
173    pub fn classify(&self) -> Classifications<'_> {
174        Classifications(Errors::new(Node::Frame(self.frame())))
175    }
176
177    classification_predicates!();
178}
179
180/// The semantic class of an error.
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182#[non_exhaustive]
183pub enum Class {
184    /// Function or method input was invalid.
185    Validation,
186    /// Stored or streamed data was malformed or internally inconsistent.
187    Corruption,
188    /// A requested resource does not exist.
189    NotFound,
190    /// Retrying the operation may succeed.
191    Retryable,
192    /// A finite resource was exhausted.
193    ResourceExhaustion(crate::ResourceExhaustionKind),
194    /// An I/O failure not normalized to another semantic class.
195    Io(std::io::ErrorKind),
196    /// An operation-specific condition identified by a stable, namespaced string.
197    ///
198    /// Functions returning this class document their tags. Tags imply no other classification;
199    /// match them with [`Classifications::has()`] instead of inspecting diagnostic text or metadata.
200    Tagged(&'static str),
201}
202
203/// A semantic class together with the concrete error which established it.
204#[derive(Clone, Copy, Debug)]
205pub struct Classification<'a> {
206    class: Class,
207    error: &'a (dyn std::error::Error + 'static),
208}
209
210/// Lazily inspect the classifications of any borrowed error, including its native sources, I/O payloads and nested
211/// [`crate::Error`] values. Unknown errors are omitted and distinct causes may yield the same classification.
212///
213/// ```
214/// let error = std::io::Error::other(gix_error::not_found("missing object"));
215/// assert!(gix_error::classify(&error).is_not_found());
216/// ```
217pub fn classify<'a>(err: &'a (dyn std::error::Error + 'static)) -> Classifications<'a> {
218    Classifications(Errors::new(err.downcast_ref::<crate::Error>().map_or(
219        Node::Source {
220            error: err,
221            location: None,
222        },
223        crate::Error::iter_root,
224    )))
225}
226
227/// A lazy iterator over classified causes. Its predicates consume the remaining iterator and stop at the first match.
228pub struct Classifications<'a>(Errors<'a>);
229
230impl<'a> Iterator for Classifications<'a> {
231    type Item = Classification<'a>;
232
233    fn next(&mut self) -> Option<Self::Item> {
234        self.0.find_map(|source| classify_one(source.error))
235    }
236}
237
238impl Classifications<'_> {
239    /// Return whether any remaining cause is explicitly marked as retryable.
240    pub fn is_retryable(self) -> bool {
241        self.has(Class::Retryable)
242    }
243
244    /// Apply the conservative retry policy of [`crate::Error::can_retry()`] to the remaining causes.
245    pub fn can_retry(mut self) -> bool {
246        self.any(|classification| class_can_retry(classification.class()))
247    }
248
249    /// Apply the broader I/O policy of [`crate::Error::can_retry_lenient()`] to the remaining causes.
250    pub fn can_retry_lenient(mut self) -> bool {
251        self.any(classification_can_retry_lenient)
252    }
253
254    /// Return whether any remaining cause reports a missing resource.
255    pub fn is_not_found(self) -> bool {
256        self.has(Class::NotFound)
257    }
258
259    /// Return whether any remaining cause reports invalid input.
260    pub fn is_validation(self) -> bool {
261        self.has(Class::Validation)
262    }
263
264    /// Return whether any remaining cause reports malformed or inconsistent data.
265    pub fn is_corrupted(self) -> bool {
266        self.has(Class::Corruption)
267    }
268
269    /// Return whether any remaining cause reports resource exhaustion.
270    pub fn is_resource_exhausted(mut self) -> bool {
271        self.any(|classification| matches!(classification.class(), Class::ResourceExhaustion(_)))
272    }
273
274    /// Return whether any remaining cause has exactly `class`, including its tag for [`Class::Tagged`].
275    pub fn has(mut self, class: Class) -> bool {
276        self.any(|classification| classification.class() == class)
277    }
278}
279
280impl<'a> Classification<'a> {
281    /// Return the semantic class.
282    pub fn class(&self) -> Class {
283        self.class
284    }
285
286    /// Return the concrete error which established the classification.
287    pub fn error(&self) -> &'a (dyn std::error::Error + 'static) {
288        self.error
289    }
290
291    /// Return the original I/O error kind, if the underlying error is an [`std::io::Error`].
292    pub fn io_kind(&self) -> Option<std::io::ErrorKind> {
293        self.error.downcast_ref::<std::io::Error>().map(std::io::Error::kind)
294    }
295}
296
297fn classify_one<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<Classification<'a>> {
298    let class = if let Some(marker) = error.downcast_ref::<crate::ClassificationMarker>() {
299        marker.class()
300    } else if let Some(error) = error.downcast_ref::<crate::Message>() {
301        error.class?
302    } else if error.is::<std::collections::TryReserveError>() {
303        Class::ResourceExhaustion(crate::ResourceExhaustionKind::AllocationFailure)
304    } else {
305        let error = error.downcast_ref::<std::io::Error>()?;
306        match error.kind() {
307            std::io::ErrorKind::NotFound => Class::NotFound,
308            std::io::ErrorKind::OutOfMemory => {
309                Class::ResourceExhaustion(crate::ResourceExhaustionKind::AllocationFailure)
310            }
311            kind => Class::Io(kind),
312        }
313    };
314    Some(Classification { class, error })
315}
316
317fn class_can_retry(class: Class) -> bool {
318    matches!(
319        class,
320        Class::Retryable | Class::Io(std::io::ErrorKind::Interrupted | std::io::ErrorKind::TimedOut)
321    )
322}
323
324fn classification_can_retry_lenient(classification: Classification<'_>) -> bool {
325    class_can_retry(classification.class())
326        || classification.io_kind().is_some_and(|kind| {
327            use std::io::ErrorKind::*;
328            matches!(
329                kind,
330                UnexpectedEof
331                    | OutOfMemory
332                    | BrokenPipe
333                    | AddrInUse
334                    | ConnectionAborted
335                    | ConnectionReset
336                    | ConnectionRefused
337            )
338        })
339}
340
341#[derive(Clone, Copy)]
342enum Node<'a> {
343    Frame(&'a crate::exn::Frame),
344    Source {
345        error: &'a (dyn std::error::Error + 'static),
346        location: Option<&'static std::panic::Location<'static>>,
347    },
348    #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
349    Chain {
350        node: &'a crate::types::ChainedError,
351        index: usize,
352        cursor: Option<usize>,
353    },
354}
355
356impl<'a> Node<'a> {
357    fn display(self) -> DisplaySource<'a> {
358        let (error, location) = match self {
359            Node::Frame(frame) => (
360                frame.error() as &(dyn std::error::Error + 'static),
361                Some(frame.location()),
362            ),
363            Node::Source { error, location } => (error, location),
364            #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
365            Node::Chain { node, .. } => (node.err.error(), node.err.has_frame_location().then_some(node.location)),
366        };
367        DisplaySource { error, location }
368    }
369
370    fn children(self) -> std::collections::VecDeque<Node<'a>> {
371        // Cause selection follows a path rather than breadth-first order, so each query needs its own chain cursor.
372        #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
373        let root = match self {
374            Node::Chain { node, index, .. } => Node::Chain {
375                node,
376                index,
377                cursor: None,
378            },
379            root => root,
380        };
381        #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
382        let root = self;
383        let mut traversal = Errors::new(root);
384        traversal.children(root);
385        traversal.pending
386    }
387
388    fn probable_cause(self) -> Option<&'a (dyn std::error::Error + 'static)> {
389        let mut node = self;
390        // Track traversal, not error addresses: a native source can share its owner's address.
391        let mut cause = None;
392        loop {
393            let mut pending = node.children();
394            let mut only_child = None;
395            while let Some(child) = pending.pop_front() {
396                if is_transparent_marker(child.display().error) {
397                    // Marker frames (including nested boundaries storing markers) are transparent, not dead ends.
398                    pending.extend(child.children());
399                } else if only_child.replace(child).is_some() {
400                    return cause;
401                }
402            }
403            node = match only_child {
404                Some(child) => child,
405                None => return cause,
406            };
407            cause = Some(node.display().error);
408        }
409    }
410}
411
412struct Errors<'a> {
413    root: Option<Node<'a>>,
414    previous: Option<Node<'a>>,
415    pending: std::collections::VecDeque<Node<'a>>,
416    #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
417    chains: Vec<(usize, Option<&'a crate::types::ChainedError>)>,
418}
419
420impl<'a> Errors<'a> {
421    fn new(root: Node<'a>) -> Self {
422        Errors {
423            root: Some(root),
424            previous: None,
425            pending: Default::default(),
426            #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
427            chains: Vec::new(),
428        }
429    }
430
431    fn source(
432        &mut self,
433        error: &'a (dyn std::error::Error + 'static),
434        location: Option<&'static std::panic::Location<'static>>,
435    ) {
436        if let Some(error) = error.downcast_ref::<crate::Error>() {
437            self.pending.push_back(error.iter_root());
438        } else if let Some(source) = native_source(error) {
439            self.pending.push_back(Node::Source {
440                error: source,
441                location: location.filter(|_| is_transparent_marker(error)),
442            });
443        }
444    }
445
446    fn children(&mut self, node: Node<'a>) {
447        match node {
448            Node::Frame(frame) => {
449                self.source(frame.error(), Some(frame.location()));
450                self.pending.extend(frame.children().iter().map(Node::Frame));
451            }
452            Node::Source { error, location } => self.source(error, location),
453            #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
454            Node::Chain { node, index, cursor } => {
455                if let Some(error) = node.err.error().downcast_ref::<crate::Error>() {
456                    self.pending.push_back(error.iter_root());
457                }
458                let cursor = match cursor {
459                    Some(cursor) => cursor,
460                    None if node.source.is_none() => return,
461                    None => {
462                        self.chains.push((index + 1, node.source.as_deref()));
463                        self.chains.len() - 1
464                    }
465                };
466                // Flattened parents occur in increasing order. One cursor per boundary streams each child once,
467                // even when other error trees are interleaved at their logical breadth-first positions.
468                let (child_index, next) = &mut self.chains[cursor];
469                // A fresh cursor for cause selection can start among siblings belonging to earlier parents.
470                while let Some(child) = next.filter(|child| child.logical_parent.is_some_and(|parent| parent < index)) {
471                    *child_index += 1;
472                    *next = child.source.as_deref();
473                }
474                while let Some(child) = next.filter(|child| child.logical_parent == Some(index)) {
475                    self.pending.push_back(Node::Chain {
476                        node: child,
477                        index: *child_index,
478                        cursor: Some(cursor),
479                    });
480                    *child_index += 1;
481                    *next = child.source.as_deref();
482                }
483            }
484        }
485    }
486}
487
488impl<'a> Iterator for Errors<'a> {
489    type Item = DisplaySource<'a>;
490
491    fn next(&mut self) -> Option<Self::Item> {
492        // Defer expansion until the caller asks for another error, so a match need not inspect any of its causes.
493        if let Some(previous) = self.previous.take() {
494            self.children(previous);
495        }
496        let node = self.root.take().or_else(|| self.pending.pop_front())?;
497        self.previous = Some(node);
498        Some(node.display())
499    }
500}
501
502impl crate::exn::Frame {
503    pub(crate) fn probable_cause_inner(&self) -> Option<&(dyn std::error::Error + 'static)> {
504        Node::Frame(self).probable_cause()
505    }
506
507    pub(crate) fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ {
508        Errors::new(Node::Frame(self)).filter(|source| !is_transparent_marker(source.error))
509    }
510}
511
512#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
513mod _impl {
514    use crate::{Error, Exn};
515    use std::fmt::Formatter;
516
517    /// Utilities
518    impl Error {
519        /// Return the error stored at this error boundary.
520        ///
521        /// This can be a classification marker hidden from [`Self::iter_errors()`], and is distinct from
522        /// [`Self::probable_cause()`].
523        pub fn error(&self) -> &(dyn std::error::Error + 'static) {
524            self.inner.frame().error()
525        }
526
527        pub(super) fn iter_root(&self) -> super::Node<'_> {
528            super::Node::Frame(self.inner.frame())
529        }
530    }
531
532    pub(crate) enum Inner {
533        ExnAsError(Box<crate::exn::Frame>),
534        Exn(Box<crate::exn::Frame>),
535    }
536
537    impl Inner {
538        pub(crate) fn frame(&self) -> &crate::exn::Frame {
539            match self {
540                Inner::ExnAsError(f) | Inner::Exn(f) => f,
541            }
542        }
543    }
544
545    impl Error {
546        /// Create a new instance representing the given `error`.
547        #[track_caller]
548        pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
549            Error {
550                inner: Inner::ExnAsError(Exn::new(error).into()),
551            }
552        }
553
554        /// Create a new instance representing an already boxed `error`.
555        #[track_caller]
556        pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
557            Self::from_error(crate::exn::Untyped::from_boxed(error))
558        }
559    }
560
561    impl std::fmt::Display for Error {
562        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
563            match &self.inner {
564                Inner::ExnAsError(err) => std::fmt::Display::fmt(err.error(), f),
565                Inner::Exn(frame) => std::fmt::Display::fmt(frame, f),
566            }
567        }
568    }
569
570    impl std::fmt::Debug for Error {
571        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
572            match &self.inner {
573                Inner::ExnAsError(err) => std::fmt::Debug::fmt(err.error(), f),
574                Inner::Exn(frame) => std::fmt::Debug::fmt(frame, f),
575            }
576        }
577    }
578
579    impl std::error::Error for Error {
580        /// Return the first source of an [Exn] error, or the source of a boxed error.
581        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
582            match &self.inner {
583                Inner::ExnAsError(frame) | Inner::Exn(frame) => {
584                    let error = frame.error();
585                    (!error.is::<Error>())
586                        .then(|| super::native_source(error))
587                        .flatten()
588                        .or_else(|| frame.children().first().map(|frame| frame.error() as _))
589                }
590            }
591        }
592    }
593
594    impl<E> From<Exn<E>> for Error
595    where
596        E: std::error::Error + Send + Sync + 'static,
597    {
598        fn from(err: Exn<E>) -> Self {
599            Error {
600                inner: Inner::Exn(err.into()),
601            }
602        }
603    }
604}
605#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
606pub(super) use _impl::Inner;
607
608#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
609mod _impl {
610    use crate::{Error, Exn};
611    use std::fmt::Formatter;
612
613    /// Utilities
614    impl Error {
615        /// Return the error stored at this error boundary.
616        ///
617        /// This can be a classification marker hidden from [`Self::iter_errors()`], and is distinct from
618        /// [`Self::probable_cause()`].
619        pub fn error(&self) -> &(dyn std::error::Error + 'static) {
620            self.inner.err.error()
621        }
622
623        pub(super) fn iter_root(&self) -> super::Node<'_> {
624            super::Node::Chain {
625                node: &self.inner,
626                index: 0,
627                cursor: None,
628            }
629        }
630    }
631
632    impl Error {
633        /// Create a new instance representing the given `error`.
634        #[track_caller]
635        pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
636            Error {
637                inner: Exn::new(error).into_chain(),
638            }
639        }
640
641        /// Create a new instance representing an already boxed `error`.
642        #[track_caller]
643        pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
644            Self::from_error(crate::exn::Untyped::from_boxed(error))
645        }
646    }
647
648    impl std::fmt::Display for Error {
649        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
650            if super::is_transparent_marker(self.error())
651                && let Some(diagnostic) = self.iter_errors_with_locations().next()
652            {
653                return std::fmt::Display::fmt(&diagnostic, f);
654            }
655            std::fmt::Display::fmt(&self.inner, f)
656        }
657    }
658
659    impl std::fmt::Debug for Error {
660        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
661            if super::is_transparent_marker(self.error())
662                && let Some(diagnostic) = self.iter_errors().next()
663            {
664                return std::fmt::Debug::fmt(diagnostic, f);
665            }
666            std::fmt::Debug::fmt(&self.inner, f)
667        }
668    }
669
670    impl std::error::Error for Error {
671        /// Return the first source of an [Exn] error, or the source of a boxed error.
672        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
673            self.inner.source()
674        }
675    }
676
677    impl<E> From<Exn<E>> for Error
678    where
679        E: std::error::Error + Send + Sync + 'static,
680    {
681        fn from(err: Exn<E>) -> Self {
682            Error {
683                inner: err.into_chain(),
684            }
685        }
686    }
687}
688
689/// Retain I/O payloads, which `std::io::Error::source()` skips even when they carry a classification or an error tree.
690pub(crate) fn native_source<'a>(
691    err: &'a (dyn std::error::Error + 'static),
692) -> Option<&'a (dyn std::error::Error + 'static)> {
693    match err.downcast_ref::<std::io::Error>() {
694        Some(err) => err.get_ref().map(|err| err as _),
695        None => err.source(),
696    }
697}
698
699pub(crate) fn is_transparent_marker(mut error: &(dyn std::error::Error + 'static)) -> bool {
700    while let Some(nested) = error.downcast_ref::<crate::Error>() {
701        error = nested.error();
702    }
703    error.is::<crate::ClassificationMarker>()
704}