Skip to main content

gix_error/exn/
impls.rs

1// Copyright 2025 FastLabs Developers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::VecDeque;
16use std::error::Error;
17use std::fmt;
18use std::marker::PhantomData;
19use std::ops::Deref;
20use std::panic::Location;
21
22use crate::concrete::chain::ErrorHandle;
23use crate::{ChainedError, Exn, write_location};
24
25impl<E: Error + Send + Sync + 'static> From<E> for Exn<E> {
26    #[track_caller]
27    fn from(error: E) -> Self {
28        Exn::new(error)
29    }
30}
31
32impl<E: Error + Send + Sync + 'static> Exn<E> {
33    /// Create a new exception with the given error.
34    ///
35    /// Its [source chain](Error::source) is retained by `error` and traversed lazily for formatting, downcasting, and
36    /// conversion. Native sources are not copied into owned [`Frame`] values and keep their concrete types.
37    ///
38    /// See also [`ErrorExt::raise`](crate::ErrorExt) for a fluent way to convert an error into an `Exn` instance.
39    #[track_caller]
40    pub fn new(error: E) -> Self {
41        let frame = Frame {
42            error: Box::new(error),
43            location: Location::caller(),
44            children: Vec::new(),
45        };
46
47        Self {
48            frame: Box::new(frame),
49            phantom: PhantomData,
50        }
51    }
52
53    /// Create a new exception with the given error and children.
54    #[track_caller]
55    pub fn raise_all<T, I>(children: I, err: E) -> Self
56    where
57        T: Error + Send + Sync + 'static,
58        I: IntoIterator,
59        I::Item: Into<Exn<T>>,
60    {
61        let mut new_exn = Exn::new(err);
62        for exn in children {
63            let exn = exn.into();
64            new_exn.frame.children.push(*exn.frame);
65        }
66        new_exn
67    }
68
69    /// Raise a new exception; this will make the current exception a child of the new one.
70    #[track_caller]
71    pub fn raise<T: Error + Send + Sync + 'static>(self, err: T) -> Exn<T> {
72        let mut new_exn = Exn::new(err);
73        new_exn.frame.children.push(*self.frame);
74        new_exn
75    }
76
77    /// Use the current exception as the head of a chain, adding `err` to its children.
78    #[track_caller]
79    pub fn chain<T: Error + Send + Sync + 'static>(mut self, err: impl Into<Exn<T>>) -> Exn<E> {
80        let err = err.into();
81        self.frame.children.push(*err.frame);
82        self
83    }
84
85    /// Use the current exception the head of a chain, adding `errors` to its children.
86    #[track_caller]
87    pub fn chain_all<T, I>(mut self, errors: I) -> Exn<E>
88    where
89        T: Error + Send + Sync + 'static,
90        I: IntoIterator,
91        I::Item: Into<Exn<T>>,
92    {
93        for err in errors {
94            let err = err.into();
95            self.frame.children.push(*err.frame);
96        }
97        self
98    }
99
100    /// Drain all explicitly added child frames of this error as untyped [`Exn`].
101    ///
102    /// Native [`Error::source()`] values remain owned by their error and aren't drainable frames. This is useful if one
103    /// wants to re-organise explicitly raised errors and the error layout is well known.
104    pub fn drain_children(&mut self) -> impl Iterator<Item = Exn> + '_ {
105        self.frame.children.drain(..).map(Exn::from)
106    }
107
108    /// Erase the type of this instance and turn it into a bare `Exn`.
109    pub fn erased(self) -> Exn {
110        let untyped_frame = {
111            let Frame {
112                error,
113                location,
114                children,
115            } = *self.frame;
116            // Unfortunately, we have to double-box here.
117            // TODO: figure out tricks to make this unnecessary.
118            let error = Untyped(error);
119            Frame {
120                error: Box::new(error),
121                location,
122                children,
123            }
124        };
125        Exn {
126            frame: Box::new(untyped_frame),
127            phantom: Default::default(),
128        }
129    }
130
131    /// Return the current exception.
132    pub fn error(&self) -> &E {
133        self.frame
134            .error
135            .downcast_ref()
136            .expect("the owned frame always matches the compile-time error type")
137    }
138
139    /// Discard all error context and return the underlying error in a Box.
140    ///
141    /// This is useful to retain the allocation, as internally it's also stored in a box,
142    /// when comparing it to [`Self::into_inner()`].
143    pub fn into_box(self) -> Box<E> {
144        match self.frame.error.downcast() {
145            Ok(err) => err,
146            Err(_) => unreachable!("The type in the frame is always the type of this instance"),
147        }
148    }
149
150    /// Discard all error context and return the underlying error.
151    ///
152    /// This may be needed to obtain something that once again implements `Error`.
153    /// Note that this destroys the internal Box and moves the value back onto the stack.
154    pub fn into_inner(self) -> E {
155        *self.into_box()
156    }
157
158    /// Turn ourselves into a top-level [Error] that implements [`std::error::Error`].
159    ///
160    /// [Error]: crate::Error
161    pub fn into_error(self) -> crate::Error {
162        self.into()
163    }
164
165    /// Convert this error tree into a chain of errors, breadth first, which flattens the tree
166    /// but retains all type dynamic type information.
167    ///
168    /// This is useful for inter-op with `anyhow`.
169    pub fn into_chain(self) -> crate::ChainedError {
170        self.into()
171    }
172
173    /// Return the underlying exception frame.
174    pub fn frame(&self) -> &Frame {
175        &self.frame
176    }
177
178    /// Iterate over all explicitly created frames in breadth-first order. The first frame is this instance, followed by
179    /// all explicitly raised children. Native [`Error::source()`] values are not frames.
180    pub fn iter(&self) -> impl Iterator<Item = &Frame> {
181        self.frame().iter_frames()
182    }
183
184    /// Find the first stored error or native source that downcasts to `T` in breadth-first order.
185    pub fn downcast_any_ref<T: Error + 'static>(&self) -> Option<&T> {
186        self.frame
187            .iter_error_nodes()
188            .find_map(|node| node.error().downcast_ref())
189    }
190}
191
192impl<E> Deref for Exn<E>
193where
194    E: Error + Send + Sync + 'static,
195{
196    type Target = E;
197
198    fn deref(&self) -> &Self::Target {
199        self.error()
200    }
201}
202
203impl<E: Error + Send + Sync + 'static> fmt::Debug for Exn<E> {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write_frame_recursive(f, self.frame(), "", ErrorMode::Display, TreeMode::Linearize)
206    }
207}
208
209impl fmt::Debug for Frame {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write_frame_recursive(f, self, "", ErrorMode::Display, TreeMode::Linearize)
212    }
213}
214
215#[derive(Copy, Clone)]
216enum ErrorMode {
217    Display,
218    Debug,
219}
220
221#[derive(Copy, Clone)]
222enum TreeMode {
223    Linearize,
224    Verbatim,
225}
226
227fn write_frame_recursive(
228    f: &mut fmt::Formatter<'_>,
229    frame: &Frame,
230    prefix: &str,
231    err_mode: ErrorMode,
232    tree_mode: TreeMode,
233) -> fmt::Result {
234    write_error_node_recursive(f, ErrorNode::Frame(frame), prefix, err_mode, tree_mode)
235}
236
237fn write_error_node_recursive(
238    f: &mut fmt::Formatter<'_>,
239    node: ErrorNode<'_>,
240    prefix: &str,
241    err_mode: ErrorMode,
242    tree_mode: TreeMode,
243) -> fmt::Result {
244    match err_mode {
245        ErrorMode::Display => fmt::Display::fmt(node.error(), f),
246        ErrorMode::Debug => write!(f, "{:?}", node.error()),
247    }?;
248    if !f.alternate() {
249        write_location(f, node.location())?;
250    }
251
252    if let Some(err) = node.error().downcast_ref::<crate::Error>() {
253        for source in err.iter_errors().filter(|source| !source.is::<crate::Error>()).skip(1) {
254            write!(f, "\n{prefix}|\n{prefix}└─ {source}")?;
255        }
256    }
257
258    let children = node.children();
259    let children_len = children.len();
260
261    for (child_index, child) in children.into_iter().enumerate() {
262        write!(f, "\n{prefix}|")?;
263        write!(f, "\n{prefix}└─ ")?;
264
265        let child_child_len = if child
266            .error()
267            .downcast_ref::<crate::Error>()
268            .is_some_and(|err| err.iter_errors().filter(|source| !source.is::<crate::Error>()).count() > 1)
269        {
270            1
271        } else {
272            child.children().len()
273        };
274        let may_linearize_chain = matches!(tree_mode, TreeMode::Linearize) && children_len == 1 && child_child_len == 1;
275        if may_linearize_chain {
276            write_error_node_recursive(f, child, prefix, err_mode, tree_mode)?;
277        } else if child_index < children_len - 1 {
278            write_error_node_recursive(f, child, &format!("{prefix}|   "), err_mode, tree_mode)?;
279        } else {
280            write_error_node_recursive(f, child, &format!("{prefix}    "), err_mode, tree_mode)?;
281        }
282    }
283
284    Ok(())
285}
286
287impl<E: Error + Send + Sync + 'static> fmt::Display for Exn<E> {
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        fmt::Display::fmt(&self.frame, f)
290    }
291}
292
293impl<E: Error + Send + Sync + 'static> PartialEq<str> for Exn<E> {
294    fn eq(&self, other: &str) -> bool {
295        crate::root_error_eq(self.frame().error(), other)
296    }
297}
298
299impl<E: Error + Send + Sync + 'static> PartialEq<&str> for Exn<E> {
300    fn eq(&self, other: &&str) -> bool {
301        <Self as PartialEq<str>>::eq(self, other)
302    }
303}
304
305impl<E: Error + Send + Sync + 'static> PartialEq<String> for Exn<E> {
306    fn eq(&self, other: &String) -> bool {
307        <Self as PartialEq<str>>::eq(self, other)
308    }
309}
310
311impl fmt::Display for Frame {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        if f.alternate() {
314            // Avoid printing alternate versions of the debug info, keep it in one line, also print the tree.
315            write_frame_recursive(f, self, "", ErrorMode::Debug, TreeMode::Verbatim)
316        } else {
317            fmt::Display::fmt(self.error(), f)
318        }
319    }
320}
321
322/// A frame in the exception tree.
323pub struct Frame {
324    /// The error that occurred at this frame.
325    error: Box<dyn Error + Send + Sync + 'static>,
326    /// The source code location where this exception frame was created.
327    location: &'static Location<'static>,
328    /// Explicitly raised child exception frames.
329    children: Vec<Frame>,
330}
331
332impl Frame {
333    /// Return the error as a reference to [`Error`].
334    ///
335    /// If the error was [erased](crate::Exn::erased), this is the original error,
336    /// so it can still be downcast to its actual type.
337    pub fn error(&self) -> &(dyn Error + Send + Sync + 'static) {
338        let mut error = &*self.error;
339        while let Some(erased) = error.downcast_ref::<Untyped>() {
340            error = &*erased.0;
341        }
342        error
343    }
344
345    /// Return the source code location where this exception frame was created.
346    /// Return the frame location used when formatting this node.
347    ///
348    /// A frame returns its own captured location. A native source inherits the location of the frame whose error owns its
349    /// source chain, providing formatting context even though no location was captured for the source itself. In contrast,
350    /// `captured_location()` reports only locations belonging to the node itself.
351    pub fn location(self) -> &'static Location<'static> {
352        self.location
353    }
354
355    /// Return explicitly raised child frames.
356    ///
357    /// Native [`Error::source()`] values are borrowed from [`Self::error()`] and traversed lazily, so they aren't owned
358    /// `Frame` children.
359    pub fn children(&self) -> &[Frame] {
360        &self.children
361    }
362}
363
364/// A borrowed node that lets one traversal visit both explicit exception frames and native [`Error::source()`] chains.
365///
366/// Explicitly raised errors are stored as [`Frame`] values, whereas native sources remain owned by their errors and
367/// must be borrowed when traversed. `Source` represents such a borrowed native error and carries forward the location
368/// of its owning frame for internal formatting without turning the source into a frame or losing its concrete type.
369#[derive(Clone, Copy)]
370pub(crate) enum ErrorNode<'a> {
371    Frame(&'a Frame),
372    Source {
373        error: &'a (dyn Error + 'static),
374        location: &'static Location<'static>,
375    },
376}
377
378impl<'a> ErrorNode<'a> {
379    pub(crate) fn error(self) -> &'a (dyn Error + 'static) {
380        match self {
381            ErrorNode::Frame(frame) => frame.error(),
382            ErrorNode::Source { error, .. } => error,
383        }
384    }
385
386    /// Return the frame location used when formatting this node.
387    ///
388    /// A frame returns its own captured location. A native source inherits the location of the frame whose error owns its
389    /// source chain, providing formatting context even though no location was captured for the source itself. In contrast,
390    /// `captured_location()` reports only locations belonging to the node itself.
391    pub(crate) fn location(self) -> &'static Location<'static> {
392        match self {
393            ErrorNode::Frame(frame) => frame.location,
394            ErrorNode::Source { location, .. } => location,
395        }
396    }
397
398    /// Return the location captured for this node itself.
399    ///
400    /// This is `Some` for an explicitly created frame and `None` for a native source. Unlike [`Self::location()`], it does
401    /// not return the owning frame's location as inherited formatting context for a source.
402    #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
403    pub(crate) fn captured_location(self) -> Option<&'static Location<'static>> {
404        match self {
405            ErrorNode::Frame(frame) => Some(frame.location),
406            ErrorNode::Source { .. } => None,
407        }
408    }
409
410    /// Return this node's immediate logical children in traversal order.
411    ///
412    /// A direct native [`Error::source()`] is first and inherits this node's formatting location. For a frame, explicitly
413    /// raised child frames follow it in insertion order. The compatibility `source()` of a nested [`crate::Error`] is
414    /// skipped because that wrapper retains an internal error graph which its own traversal APIs expand separately;
415    /// following the compatibility source here would expose only one path and duplicate that expansion.
416    pub(crate) fn children(self) -> Vec<ErrorNode<'a>> {
417        let error = self.error();
418        let location = self.location();
419        let mut children = Vec::new();
420        if !error.is::<crate::Error>() {
421            if let Some(error) = error.source() {
422                children.push(ErrorNode::Source { error, location });
423            }
424        }
425        if let ErrorNode::Frame(frame) = self {
426            children.extend(frame.children.iter().map(ErrorNode::Frame));
427        }
428        children
429    }
430
431    fn same(self, other: ErrorNode<'_>) -> bool {
432        std::ptr::addr_eq(self.error(), other.error())
433    }
434}
435
436/// Navigation
437impl Frame {
438    /// Find the best possible cause:
439    ///
440    /// * in a linear chain of a single error each, it's the last-most error
441    /// * in trees, find the deepest-possible error that has the most leafs as children
442    ///
443    /// Native [`Error::source()`] values participate as borrowed children. Return `None` if there are no children.
444    pub fn probable_cause(&self) -> Option<&(dyn Error + 'static)> {
445        self.probable_cause_node().map(ErrorNode::error)
446    }
447
448    pub(crate) fn probable_cause_node(&self) -> Option<ErrorNode<'_>> {
449        /// Perform a recursive depth-first, post-order walk to select a probable-cause candidate.
450        ///
451        /// The returned tuple contains the number of leaves below `node`, the depth of the selected candidate, and the
452        /// candidate itself. After visiting all children, the current node competes with the best descendant: the candidate
453        /// representing more leaves wins, with greater depth breaking ties. Exact ties between siblings retain the first
454        /// child in traversal order.
455        fn walk(node: ErrorNode<'_>, depth: usize) -> (usize, usize, ErrorNode<'_>) {
456            let children = node.children();
457            if children.is_empty() {
458                return (1, depth, node);
459            }
460
461            let mut total_leafs = 0;
462            let mut best: Option<(usize, usize, ErrorNode<'_>)> = None;
463
464            for child in children {
465                let (leafs, child_depth, candidate) = walk(child, depth + 1);
466                total_leafs += leafs;
467
468                match best {
469                    None => best = Some((leafs, child_depth, candidate)),
470                    Some((best_leafs, best_depth, _)) => {
471                        if leafs > best_leafs || (leafs == best_leafs && child_depth > best_depth) {
472                            best = Some((leafs, child_depth, candidate));
473                        }
474                    }
475                }
476            }
477
478            let self_candidate = (total_leafs, depth, node);
479            match best {
480                None => self_candidate,
481                Some(best_child) => {
482                    if total_leafs > best_child.0 || (total_leafs == best_child.0 && depth > best_child.1) {
483                        self_candidate
484                    } else {
485                        best_child
486                    }
487                }
488            }
489        }
490
491        let root = ErrorNode::Frame(self);
492        let children = root.children();
493        if children.iter().all(|child| child.children().is_empty()) {
494            if let Some(last) = children.last() {
495                return Some(*last);
496            }
497        }
498
499        let cause = walk(root, 0).2;
500        (!cause.same(root)).then_some(cause)
501    }
502
503    /// Iterate over all explicitly created frames in breadth-first order. The first frame is this instance, followed by
504    /// all explicitly raised children. Native [`Error::source()`] values are not frames.
505    pub fn iter_frames(&self) -> impl Iterator<Item = &Frame> + '_ {
506        let mut queue = std::collections::VecDeque::new();
507        queue.push_back(self);
508        BreadthFirstFrames { queue }
509    }
510
511    pub(crate) fn iter_error_nodes(&self) -> BreadthFirstErrorNodes<'_> {
512        let mut queue = VecDeque::new();
513        queue.push_back(ErrorNode::Frame(self));
514        BreadthFirstErrorNodes { queue }
515    }
516}
517
518/// Breadth-first iterator over explicitly created `Frame`s.
519pub struct BreadthFirstFrames<'a> {
520    queue: std::collections::VecDeque<&'a Frame>,
521}
522
523impl<'a> Iterator for BreadthFirstFrames<'a> {
524    type Item = &'a Frame;
525
526    fn next(&mut self) -> Option<Self::Item> {
527        let frame = self.queue.pop_front()?;
528        for child in frame.children() {
529            self.queue.push_back(child);
530        }
531        Some(frame)
532    }
533}
534
535pub(crate) struct BreadthFirstErrorNodes<'a> {
536    queue: VecDeque<ErrorNode<'a>>,
537}
538
539impl<'a> Iterator for BreadthFirstErrorNodes<'a> {
540    type Item = ErrorNode<'a>;
541
542    fn next(&mut self) -> Option<Self::Item> {
543        let node = self.queue.pop_front()?;
544        self.queue.extend(node.children());
545        Some(node)
546    }
547}
548
549impl<E> From<Exn<E>> for Box<Frame>
550where
551    E: Error + Send + Sync + 'static,
552{
553    fn from(err: Exn<E>) -> Self {
554        err.frame
555    }
556}
557
558impl<E> From<Exn<E>> for Box<dyn Error + Send + Sync + 'static>
559where
560    E: Error + Send + Sync + 'static,
561{
562    fn from(err: Exn<E>) -> Self {
563        Box::new(err.into_error())
564    }
565}
566
567#[cfg(feature = "anyhow")]
568impl<E> From<Exn<E>> for anyhow::Error
569where
570    E: Error + Send + Sync + 'static,
571{
572    fn from(err: Exn<E>) -> Self {
573        anyhow::Error::from(err.into_chain())
574    }
575}
576
577impl<E> From<Exn<E>> for Frame
578where
579    E: Error + Send + Sync + 'static,
580{
581    fn from(err: Exn<E>) -> Self {
582        *err.frame
583    }
584}
585
586impl From<Frame> for Exn {
587    fn from(frame: Frame) -> Self {
588        Exn {
589            frame: Box::new(frame),
590            phantom: Default::default(),
591        }
592    }
593}
594
595/// A marker to show that type information is not available,
596/// while storing all extractable information about the erased type.
597/// It's the default type for [Exn].
598pub struct Untyped(Box<dyn Error + Send + Sync + 'static>);
599
600impl Untyped {
601    pub(crate) fn from_boxed(error: Box<dyn Error + Send + Sync + 'static>) -> Self {
602        Untyped(error)
603    }
604}
605
606impl fmt::Display for Untyped {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        fmt::Display::fmt(&self.0, f)
609    }
610}
611
612impl fmt::Debug for Untyped {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        fmt::Debug::fmt(&self.0, f)
615    }
616}
617
618impl Error for Untyped {
619    fn source(&self) -> Option<&(dyn Error + 'static)> {
620        self.0.source()
621    }
622}
623
624/// An error that merely says that something is wrong.
625pub struct Something;
626
627impl fmt::Display for Something {
628    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
629        f.write_str("Something went wrong")
630    }
631}
632
633impl fmt::Debug for Something {
634    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635        fmt::Display::fmt(&self, f)
636    }
637}
638
639impl Error for Something {}
640
641impl<E> From<Exn<E>> for ChainedError
642where
643    E: std::error::Error + Send + Sync + 'static,
644{
645    fn from(err: Exn<E>) -> Self {
646        let probable_cause = err
647            .frame
648            .probable_cause_node()
649            .and_then(|cause| err.frame.iter_error_nodes().position(|node| node.same(cause)));
650        let flattened = flatten_error_nodes(*err.frame);
651        let mut source = None;
652        let leaves_to_root = flattened.into_iter().enumerate().rev();
653        for (index, node) in leaves_to_root {
654            source = Some(Box::new(ChainedError {
655                err: node.error,
656                location: node.location,
657                is_probable_cause: probable_cause.map_or(index == 0, |cause| cause == index),
658                logical_parent: node.logical_parent,
659                source,
660            }));
661        }
662        *source.expect("an Exn always contains its root error")
663    }
664}
665
666struct OwnedErrorNode {
667    error: ErrorHandle,
668    location: &'static Location<'static>,
669    logical_parent: Option<usize>,
670}
671
672/// Consume an exception-frame tree and flatten its errors into logical breadth-first order for [`ChainedError`].
673///
674/// Each frame's direct native [`Error::source()`] is queued before its explicitly raised child frames, and subsequent
675/// native sources continue as children of the preceding source. Every output node retains an owning [`ErrorHandle`], the
676/// frame location used for formatting, and the output index of its logical parent so the tree relationships can later be
677/// reconstructed. Native sources inherit their owning frame's location.
678///
679/// A nested [`crate::Error`] is retained as one node without following its compatibility `source()` chain. Its internal
680/// graph is expanded separately by the [`crate::Error`] traversal APIs, avoiding a partial and duplicated representation.
681fn flatten_error_nodes(root: Frame) -> Vec<OwnedErrorNode> {
682    enum Pending {
683        Frame {
684            frame: Frame,
685            logical_parent: Option<usize>,
686        },
687        Source {
688            error: ErrorHandle,
689            location: &'static Location<'static>,
690            logical_parent: usize,
691        },
692    }
693
694    let mut queue = VecDeque::from([Pending::Frame {
695        frame: root,
696        logical_parent: None,
697    }]);
698    let mut out = Vec::new();
699    while let Some(node) = queue.pop_front() {
700        let node_index = out.len();
701        match node {
702            Pending::Frame {
703                frame:
704                    Frame {
705                        error,
706                        location,
707                        children,
708                    },
709                logical_parent,
710            } => {
711                let error = ErrorHandle::new(unerase(error));
712                if !error.error().is::<crate::Error>() {
713                    if let Some(source) = error.source() {
714                        queue.push_back(Pending::Source {
715                            error: source,
716                            location,
717                            logical_parent: node_index,
718                        });
719                    }
720                }
721                queue.extend(children.into_iter().map(|frame| Pending::Frame {
722                    frame,
723                    logical_parent: Some(node_index),
724                }));
725                out.push(OwnedErrorNode {
726                    error,
727                    location,
728                    logical_parent,
729                });
730            }
731            Pending::Source {
732                error,
733                location,
734                logical_parent,
735            } => {
736                if !error.error().is::<crate::Error>() {
737                    if let Some(source) = error.source() {
738                        queue.push_back(Pending::Source {
739                            error: source,
740                            location,
741                            logical_parent: node_index,
742                        });
743                    }
744                }
745                out.push(OwnedErrorNode {
746                    error,
747                    location,
748                    logical_parent: Some(logical_parent),
749                });
750            }
751        }
752    }
753    out
754}
755
756/// Remove all type-erasure markers before storing an error in a [`ChainedError`].
757///
758/// [`Untyped::source()`] deliberately forwards to the wrapped error's source to keep
759/// the marker transparent. Storing the marker itself in the chain would therefore
760/// hide a wrapped leaf error from source traversal and classification. Unwrapping it
761/// here retains the original runtime type without changing those source semantics.
762fn unerase(mut error: Box<dyn Error + Send + Sync + 'static>) -> Box<dyn Error + Send + Sync + 'static> {
763    loop {
764        match error.downcast::<Untyped>() {
765            Ok(untyped) => error = untyped.0,
766            Err(typed) => return typed,
767        }
768    }
769}