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}