gix_error/error.rs
1/// A borrowed error together with its optional caller location, intended for diagnostic display.
2///
3/// Errors owned by a [`crate::Frame`] have the location captured when that frame was created. Native
4/// [`std::error::Error::source()`] values have no location because no caller location was captured for them.
5///
6/// Unlike [`crate::Frame`], this type neither owns the error nor represents relationships in an error tree. This lets
7/// [`crate::Error::iter_errors_with_locations()`] provide the same lightweight view for the tree-backed and flattened-chain
8/// representations.
9///
10/// Its normal [`Display`](std::fmt::Display) output appends the location when one is available. Alternate formatting
11/// (`{source:#}`) forwards alternate formatting to the underlying error and always omits the location.
12#[derive(Clone, Copy, Debug)]
13pub struct DisplaySource<'a> {
14 error: &'a (dyn std::error::Error + 'static),
15 location: Option<&'static std::panic::Location<'static>>,
16}
17
18impl<'a> DisplaySource<'a> {
19 /// Return the stored error, preserving its concrete type for downcasting.
20 pub fn error(&self) -> &'a (dyn std::error::Error + 'static) {
21 self.error
22 }
23
24 /// Return the caller location captured for this error frame, or `None` for a native error source.
25 pub fn location(&self) -> Option<&'static std::panic::Location<'static>> {
26 self.location
27 }
28}
29
30impl std::fmt::Display for DisplaySource<'_> {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 std::fmt::Display::fmt(self.error, f)?;
33 if !f.alternate() {
34 if let Some(location) = self.location {
35 crate::write_location(f, location)?;
36 }
37 }
38 Ok(())
39 }
40}
41
42impl crate::Error {
43 /// Find the first stored error or native source that downcasts to `T` in logical breadth-first order.
44 pub fn downcast_any_ref<T: std::error::Error + 'static>(&self) -> Option<&T> {
45 self.iter_errors().find_map(|error| error.downcast_ref())
46 }
47}
48
49#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
50mod _impl {
51 use crate::{DisplaySource, Error, Exn};
52 use std::fmt::Formatter;
53
54 /// Utilities
55 impl Error {
56 /// Return the error stored at this error boundary.
57 ///
58 /// This is the first error yielded by [`Self::iter_errors()`] and is distinct from
59 /// [`Self::probable_cause()`].
60 pub fn error(&self) -> &(dyn std::error::Error + 'static) {
61 self.inner.frame().error()
62 }
63
64 /// Return the error that is most likely the root cause, based on heuristics.
65 /// Note that if there is nothing but this error, i.e. no source or children, this error is returned.
66 pub fn probable_cause(&self) -> &(dyn std::error::Error + 'static) {
67 let root = self.inner.frame();
68 let cause = root.probable_cause().unwrap_or_else(|| root.error());
69 cause.downcast_ref::<Error>().map_or(cause, Error::probable_cause)
70 }
71
72 /// Return the stored error and all frame errors and native sources reachable from it, recursively expanding nested
73 /// [`Error`] values.
74 ///
75 /// The first item is the error stored inside this [`Error`], not this `Error` wrapper. Remaining errors are ordered
76 /// logically breadth-first; a frame's direct native source precedes its explicitly raised child frames.
77 /// These are references to the underlying errors, so their concrete types remain available for *downcasting* and
78 /// their own [`Display`](std::fmt::Display) implementations can be used. Use
79 /// [`Self::iter_errors_with_locations()`] to access caller locations.
80 pub fn iter_errors(&self) -> impl Iterator<Item = &(dyn std::error::Error + 'static)> + '_ {
81 self.collect_errors_with_locations()
82 .into_iter()
83 .map(|source| source.error)
84 }
85
86 /// Return the same errors as [`Self::iter_errors()`], paired with their captured caller locations where available
87 /// and in the same order.
88 pub fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ {
89 self.collect_errors_with_locations().into_iter()
90 }
91
92 fn collect_errors_with_locations(&self) -> Vec<DisplaySource<'_>> {
93 let mut queue = std::collections::VecDeque::from([crate::exn::ErrorNode::Frame(self.inner.frame())]);
94 let mut out = Vec::new();
95 while let Some(node) = queue.pop_front() {
96 let error = node.error();
97 out.push(DisplaySource {
98 error,
99 location: node.captured_location(),
100 });
101 if let Some(error) = error.downcast_ref::<Error>() {
102 queue.push_back(crate::exn::ErrorNode::Frame(error.inner.frame()));
103 }
104 queue.extend(node.children());
105 }
106 out
107 }
108
109 /// Return `true` if any stored error, or an error in its [`source()`](std::error::Error::source) chain, is:
110 ///
111 /// * explicitly marked with [`RetryableError`](crate::RetryableError), or
112 /// * an [`std::io::Error`] with kind `Interrupted`, `UnexpectedEof`, `OutOfMemory`, `TimedOut`, `BrokenPipe`,
113 /// `AddrInUse`, `ConnectionAborted`, `ConnectionReset`, or `ConnectionRefused`.
114 ///
115 /// Nested [`Error`] values are inspected recursively. `false` only means that no known retryable error was
116 /// found; it does not guarantee that retrying cannot succeed.
117 pub fn can_retry(&self) -> bool {
118 self.iter_errors().any(super::is_retryable)
119 }
120
121 /// Return `true` if malformed or internally inconsistent data caused the failure.
122 pub fn is_corrupted(&self) -> bool {
123 self.iter_errors().any(super::is_corrupted)
124 }
125
126 /// Return `true` if a requested resource was not found.
127 pub fn is_not_found(&self) -> bool {
128 self.iter_errors().any(super::is_not_found)
129 }
130
131 /// Return `true` if invalid input caused the failure.
132 pub fn is_validation(&self) -> bool {
133 self.iter_errors().any(super::is_validation)
134 }
135 }
136
137 pub(crate) enum Inner {
138 ExnAsError(Box<crate::exn::Frame>),
139 Exn(Box<crate::exn::Frame>),
140 }
141
142 impl Inner {
143 fn frame(&self) -> &crate::exn::Frame {
144 match self {
145 Inner::ExnAsError(f) | Inner::Exn(f) => f,
146 }
147 }
148 }
149
150 impl Error {
151 /// Create a new instance representing the given `error`.
152 #[track_caller]
153 pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
154 Error {
155 inner: Inner::ExnAsError(Exn::new(error).into()),
156 }
157 }
158
159 /// Create a new instance representing an already boxed `error`.
160 #[track_caller]
161 pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
162 Self::from_error(crate::Untyped::from_boxed(error))
163 }
164 }
165
166 impl std::fmt::Display for Error {
167 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
168 match &self.inner {
169 Inner::ExnAsError(err) => std::fmt::Display::fmt(err.error(), f),
170 Inner::Exn(frame) => std::fmt::Display::fmt(frame, f),
171 }
172 }
173 }
174
175 impl std::fmt::Debug for Error {
176 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
177 match &self.inner {
178 Inner::ExnAsError(err) => std::fmt::Debug::fmt(err.error(), f),
179 Inner::Exn(frame) => std::fmt::Debug::fmt(frame, f),
180 }
181 }
182 }
183
184 impl std::error::Error for Error {
185 /// Return the first source of an [Exn] error, or the source of a boxed error.
186 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
187 match &self.inner {
188 Inner::ExnAsError(frame) | Inner::Exn(frame) => {
189 let error = frame.error();
190 (!error.is::<Error>())
191 .then(|| error.source())
192 .flatten()
193 .or_else(|| frame.children().first().map(|frame| frame.error() as _))
194 }
195 }
196 }
197 }
198
199 impl<E> From<Exn<E>> for Error
200 where
201 E: std::error::Error + Send + Sync + 'static,
202 {
203 fn from(err: Exn<E>) -> Self {
204 Error {
205 inner: Inner::Exn(err.into()),
206 }
207 }
208 }
209}
210#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
211pub(super) use _impl::Inner;
212
213#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
214mod _impl {
215 use crate::{DisplaySource, Error, Exn};
216 use std::fmt::Formatter;
217
218 /// A temporary adjacency-list node used to recover the logical error graph from flattened [`crate::ChainedError`]
219 /// values.
220 ///
221 /// Auto-chain storage is a linked list for compatibility with [`std::error::Error::source()`]. The iteration APIs
222 /// rebuild its retained parent relationships in a `Vec<ErrorGraphNode>` so nested [`Error`] values can join the same
223 /// breadth-first traversal without changing that source-chain representation.
224 struct ErrorGraphNode<'a> {
225 /// The borrowed error and optional frame location yielded for this node.
226 source: DisplaySource<'a>,
227 /// Indices of logical children in the temporary node vector, in traversal order.
228 children: Vec<usize>,
229 }
230
231 /// Utilities
232 impl Error {
233 /// Return the error stored at this error boundary.
234 ///
235 /// This is the first error yielded by [`Self::iter_errors()`] and is distinct from
236 /// [`Self::probable_cause()`].
237 pub fn error(&self) -> &(dyn std::error::Error + 'static) {
238 self.inner.err.error()
239 }
240
241 /// Return the error that is most likely the root cause, based on heuristics.
242 /// Note that if there is nothing but this error, i.e. no source or children, this error is returned.
243 pub fn probable_cause(&self) -> &(dyn std::error::Error + 'static) {
244 let cause = std::iter::successors(Some(&self.inner), |err| err.source.as_deref())
245 .find(|err| err.is_probable_cause)
246 .map_or(self as &(dyn std::error::Error + 'static), |err| err.err.error());
247 cause.downcast_ref::<Error>().map_or(cause, Error::probable_cause)
248 }
249
250 /// Return the stored error and all frame errors and native sources reachable from it, recursively expanding nested
251 /// [`Error`] values.
252 ///
253 /// The first item is the error stored inside this [`Error`], not this `Error` wrapper. Remaining errors retain the
254 /// logical breadth-first order captured while flattening; a frame's direct native source precedes its
255 /// explicitly raised child frames. These are references to the underlying errors, so their concrete types remain
256 /// available for downcasting and their own [`Display`](std::fmt::Display) implementations can be used. Use
257 /// [`Self::iter_errors_with_locations()`] to access caller locations.
258 pub fn iter_errors(&self) -> impl Iterator<Item = &(dyn std::error::Error + 'static)> + '_ {
259 self.collect_errors_with_locations()
260 .into_iter()
261 .map(|source| source.error)
262 }
263
264 /// Return the same errors as [`Self::iter_errors()`], paired with their captured caller locations where available
265 /// and in the same order.
266 ///
267 /// The stored error, rather than this `Error` wrapper, is the first item. Frame errors have the location
268 /// captured when their frame was created; native sources have no caller location of their own. This enables opt-in
269 /// caller traces without making locations part of the underlying errors or their
270 /// [`Display`](std::fmt::Display) implementations. Each [`DisplaySource`] exposes both values for custom rendering,
271 /// and its own [`Display`](std::fmt::Display) implementation appends an available location by default.
272 pub fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ {
273 self.collect_errors_with_locations().into_iter()
274 }
275
276 fn collect_errors_with_locations(&self) -> Vec<DisplaySource<'_>> {
277 let mut graph = Vec::new();
278 let (root, nested) = self.append_error_chain(&mut graph);
279 let mut pending = std::collections::VecDeque::from(nested);
280 while let Some((parent, error)) = pending.pop_front() {
281 let (nested_root, more_nested) = error.append_error_chain(&mut graph);
282 graph[parent].children.insert(0, nested_root);
283 pending.extend(more_nested);
284 }
285
286 let mut queue = std::collections::VecDeque::from([root]);
287 let mut out = Vec::new();
288 while let Some(index) = queue.pop_front() {
289 let node = &graph[index];
290 out.push(node.source);
291 queue.extend(node.children.iter().copied());
292 }
293 out
294 }
295
296 /// Append this error boundary's flattened chain to `graph` and reconstruct its local parent-child relationships.
297 ///
298 /// The first tuple value is the graph index of the stored error. Each entry in the second value contains the graph
299 /// index of a node whose error is another [`Error`], paired with that nested boundary. The caller appends those
300 /// nested chains iteratively and connects each returned root as the wrapper node's first child, avoiding recursive
301 /// graph construction before the completed graph is traversed breadth-first.
302 fn append_error_chain<'a>(&'a self, graph: &mut Vec<ErrorGraphNode<'a>>) -> (usize, Vec<(usize, &'a Error)>) {
303 let chain = std::iter::successors(Some(&self.inner), |err| err.source.as_deref()).collect::<Vec<_>>();
304 let root = graph.len();
305 graph.extend(chain.iter().map(|chained| ErrorGraphNode {
306 source: DisplaySource {
307 error: chained.err.error(),
308 location: (!chained.err.is_native_source()).then_some(chained.location),
309 },
310 children: Vec::new(),
311 }));
312
313 for (index, chained) in chain.iter().enumerate() {
314 if let Some(parent) = chained.logical_parent {
315 graph[root + parent].children.push(root + index);
316 }
317 }
318 let nested = chain
319 .into_iter()
320 .enumerate()
321 .filter_map(|(index, chained)| {
322 chained
323 .err
324 .error()
325 .downcast_ref::<Error>()
326 .map(|error| (root + index, error))
327 })
328 .collect();
329 (root, nested)
330 }
331
332 /// Return `true` if any stored error, or an error in its [`source()`](std::error::Error::source) chain, is:
333 ///
334 /// * explicitly marked with [`RetryableError`](crate::RetryableError), or
335 /// * an [`std::io::Error`] with kind `Interrupted`, `UnexpectedEof`, `OutOfMemory`, `TimedOut`, `BrokenPipe`,
336 /// `AddrInUse`, `ConnectionAborted`, `ConnectionReset`, or `ConnectionRefused`.
337 ///
338 /// Nested [`Error`] values are inspected recursively. `false` only means that no known retryable error was
339 /// found; it does not guarantee that retrying cannot succeed.
340 pub fn can_retry(&self) -> bool {
341 self.iter_errors().any(super::is_retryable)
342 }
343
344 /// Return `true` if malformed or internally inconsistent data caused the failure.
345 pub fn is_corrupted(&self) -> bool {
346 self.iter_errors().any(super::is_corrupted)
347 }
348
349 /// Return `true` if a requested resource was not found.
350 pub fn is_not_found(&self) -> bool {
351 self.iter_errors().any(super::is_not_found)
352 }
353
354 /// Return `true` if invalid input caused the failure.
355 pub fn is_validation(&self) -> bool {
356 self.iter_errors().any(super::is_validation)
357 }
358 }
359
360 impl Error {
361 /// Create a new instance representing the given `error`.
362 #[track_caller]
363 pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
364 Error {
365 inner: Exn::new(error).into_chain(),
366 }
367 }
368
369 /// Create a new instance representing an already boxed `error`.
370 #[track_caller]
371 pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
372 Self::from_error(crate::Untyped::from_boxed(error))
373 }
374 }
375
376 impl std::fmt::Display for Error {
377 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
378 std::fmt::Display::fmt(&self.inner, f)
379 }
380 }
381
382 impl std::fmt::Debug for Error {
383 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
384 std::fmt::Debug::fmt(&self.inner, f)
385 }
386 }
387
388 impl std::error::Error for Error {
389 /// Return the first source of an [Exn] error, or the source of a boxed error.
390 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
391 self.inner.source()
392 }
393 }
394
395 impl<E> From<Exn<E>> for Error
396 where
397 E: std::error::Error + Send + Sync + 'static,
398 {
399 fn from(err: Exn<E>) -> Self {
400 Error {
401 inner: err.into_chain(),
402 }
403 }
404 }
405}
406
407/// Return `true` if `err` or any error in its [`source()`](std::error::Error::source) chain is explicitly marked with
408/// [`RetryableError`](crate::RetryableError), or is an [`std::io::Error`] whose kind is `Interrupted`, `UnexpectedEof`,
409/// `OutOfMemory`, `TimedOut`, `BrokenPipe`, `AddrInUse`, `ConnectionAborted`, `ConnectionReset`, or `ConnectionRefused`.
410///
411/// Nested [`crate::Error`] values are inspected recursively. `false` only means that no known retryable error was found; it
412/// does not guarantee that retrying cannot succeed.
413pub fn can_retry(err: &(dyn std::error::Error + 'static)) -> bool {
414 is_retryable(err)
415}
416
417fn is_retryable(err: &(dyn std::error::Error + 'static)) -> bool {
418 error_chain(err).any(|err| {
419 if let Some(err) = err.downcast_ref::<crate::Error>() {
420 return err.can_retry();
421 }
422 if err.is::<crate::RetryableError>() {
423 return true;
424 }
425 let Some(err) = err.downcast_ref::<std::io::Error>() else {
426 return false;
427 };
428 use std::io::ErrorKind::*;
429 matches!(
430 err.kind(),
431 Interrupted
432 | UnexpectedEof
433 | OutOfMemory
434 | TimedOut
435 | BrokenPipe
436 | AddrInUse
437 | ConnectionAborted
438 | ConnectionReset
439 | ConnectionRefused
440 )
441 })
442}
443
444fn is_corrupted(err: &(dyn std::error::Error + 'static)) -> bool {
445 error_chain(err).any(|err| {
446 err.downcast_ref::<crate::Error>()
447 .is_some_and(crate::Error::is_corrupted)
448 || err.is::<crate::CorruptionError>()
449 })
450}
451
452fn is_not_found(err: &(dyn std::error::Error + 'static)) -> bool {
453 error_chain(err).any(|err| {
454 err.downcast_ref::<crate::Error>()
455 .is_some_and(crate::Error::is_not_found)
456 || err.is::<crate::NotFoundError>()
457 || err
458 .downcast_ref::<std::io::Error>()
459 .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound)
460 })
461}
462
463fn is_validation(err: &(dyn std::error::Error + 'static)) -> bool {
464 error_chain(err).any(|err| {
465 err.downcast_ref::<crate::Error>()
466 .is_some_and(crate::Error::is_validation)
467 || err.is::<crate::ValidationError>()
468 })
469}
470
471fn error_chain<'a>(
472 err: &'a (dyn std::error::Error + 'static),
473) -> impl Iterator<Item = &'a (dyn std::error::Error + 'static)> {
474 std::iter::successors(Some(err), |err| err.source())
475}