display_full_error/
lib.rs

1//! Display Full Error - Minimal display formatter for error chains
2//!
3//! This library provides the [`DisplayFullError`] wrapper type to format
4//! [errors](::core::error::Error) with their chain of
5//! [sources](core::error::Error::source).
6//!
7//! Error messages are formatted on a single line, separated with `: `; up to
8//! 1024 messages per chain are printed, after which a single `: ...` is printed.
9//!
10//! That's all there is to it, there is no extra configuration or advanced
11//! features. This is intended as the most minimal formatter supporting error
12//! sources, to address the fact that there's no helper in the standard library
13//! so far as of Rust 1.85 (2025-03). If a standard formatter supporting error
14//! sources is added, this crate will be deprecated (but remain available).
15//!
16//! As a convenience, this library also exposes the [`DisplayFullErrorExt`]
17//! trait. It adds the [`display_full`](DisplayFullErrorExt::display_full)
18//! method to errors which returns the error in the formatting wrapper, as well
19//! as the [`to_string_full`](DisplayFullErrorExt::to_string_full) method as
20//! a shorthand for `.display_full().to_string()`.
21//!
22//! ```rust
23//! use ::core::{error, fmt};
24//!
25//! use ::display_full_error::{DisplayFullError, DisplayFullErrorExt};
26//!
27//! // main error
28//! #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
29//! enum UploadError {
30//!   Permission(PermissionError),
31//!   Limit(LimitError),
32//! }
33//! impl fmt::Display for UploadError {
34//!   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35//!     f.write_str("upload failed")
36//!   }
37//! }
38//! impl error::Error for UploadError {
39//!   fn source(&self) -> Option<&(dyn error::Error + 'static)> {
40//!     Some(match self {
41//!       UploadError::Permission(e) => e,
42//!       UploadError::Limit(e) => e,
43//!     })
44//!   }
45//! }
46//!
47//! // first source error
48//! #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49//! struct PermissionError;
50//! impl fmt::Display for PermissionError {
51//!   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52//!     f.write_str("permission denied")
53//!   }
54//! }
55//! impl error::Error for PermissionError {}
56//!
57//! // second source error
58//! #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59//! struct LimitError;
60//! impl fmt::Display for LimitError {
61//!   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62//!     f.write_str("upload exceeds max limit")
63//!   }
64//! }
65//! impl error::Error for LimitError {}
66//!
67//! // usage example
68//! let err = UploadError::Permission(PermissionError);
69//!
70//! // You can use the wrapper directly, e.g. in a `format!`
71//! assert_eq!(format!("the app crashed: {}", DisplayFullError(&err)), String::from("the app crashed: upload failed: permission denied"));
72//! // Or you can use `to_string`
73//! assert_eq!(DisplayFullError(&err).to_string(), String::from("upload failed: permission denied"));
74//! // You can also use the convenience methods from the extension trait
75//! assert_eq!(format!("the app crashed: {}", err.display_full()), String::from("the app crashed: upload failed: permission denied"));
76//! // `to_string_full` requires the `alloc` feature to be enabled
77//! #[cfg(feature = "alloc")]
78//! assert_eq!(err.to_string_full(), String::from("upload failed: permission denied"));
79//! ```
80//!
81//! This library requires Rust 1.81.0 or later as it depends on the Rust
82//! feature `error_in_core`. This library is compatible with `no_std`. There
83//! are no dependencies or optional features. This library does not introduce
84//! any runtime panics. It is recommended to use this library as an internal
85//! helper and to avoid leaking it into your public APIs. The output is
86//! guaranteed to be stable, any change would cause a major version bump.
87//!
88//! The formatting uses `: ` as it follows existing conventions and allows to
89//! keep the formatted error on a single line if the error messages don't
90//! include newlines. Keeping the error on a single line increases compatibility
91//! with tools handling error output.
92//!
93//! The maximum number of messages could have been a const parameter, but making
94//! it so currently harms ergonomics quite a lot as there is no support for
95//! default const values as of Rust 1.83. See the following Rust issues:
96//! [#27336](https://github.com/rust-lang/rust/issues/27336),
97//! [#85077](https://github.com/rust-lang/rust/issues/85077).
98#![deny(missing_docs)]
99#![no_std]
100#[cfg(any(test, feature = "alloc"))]
101extern crate alloc;
102
103/// Maximum number of messages to print in a single full error.
104///
105/// This value includes the initial error. If there are more errors left, the
106/// next error will be printed as `...` and formatting will end.
107pub const MESSAGE_LIMIT: u16 = 1024;
108
109/// Formatting wrapper to display errors, including their sources.
110///
111/// Error messages are formatted on a single line, separated with `: `; up to
112/// 1024 messages per chain are printed, after which a single `: ...` is printed.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
114pub struct DisplayFullError<'e, E>(pub &'e E)
115where
116  E: ::core::error::Error + ?Sized;
117
118impl<E> ::core::fmt::Display for DisplayFullError<'_, E>
119where
120  E: ::core::error::Error + ?Sized,
121{
122  fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
123    core::fmt::Display::fmt(&self.0, f)?;
124    let mut printed: u16 = 1;
125    for e in ::core::iter::successors(self.0.source(), |e| e.source()) {
126      if printed >= MESSAGE_LIMIT {
127        f.write_str(": ...")?;
128        return Ok(());
129      }
130      f.write_str(": ")?;
131      ::core::fmt::Display::fmt(e, f)?;
132      printed = printed.saturating_add(1);
133    }
134    Ok(())
135  }
136}
137
138/// Private module, to implement the trait sealing pattern.
139mod private {
140  /// To restrict `DisplayFullErrorExt` implementations to this crate.
141  pub trait Sealed {}
142}
143
144/// Extension trait providing convenience methods on [errors](::core::error::Error).
145///
146/// This trait provides a blanket implementation for all types implementing [the standard `Error` trait](::core::error::Error).
147pub trait DisplayFullErrorExt: ::core::error::Error + private::Sealed {
148  /// Get a reference to this error wrapped in a [`DisplayFullError`] formatter, to display the error with all its sources.
149  fn display_full(&self) -> DisplayFullError<'_, Self> {
150    DisplayFullError(self)
151  }
152
153  /// Shorthand for `.display_full().to_string()`
154  ///
155  /// Requires the `alloc` feature.
156  #[cfg(feature = "alloc")]
157  fn to_string_full(&self) -> alloc::string::String {
158    use crate::alloc::string::ToString;
159
160    self.display_full().to_string()
161  }
162}
163
164impl<E> private::Sealed for E where E: ::core::error::Error + ?Sized {}
165
166impl<E> DisplayFullErrorExt for E where E: ::core::error::Error + ?Sized {}
167
168#[cfg(test)]
169mod tests {
170  use super::*;
171  use ::alloc::format;
172  use ::alloc::string::{String, ToString};
173  use ::core::{error, fmt};
174
175  #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
176  enum UploadError {
177    Permission(PermissionError),
178    #[allow(dead_code)]
179    Limit(LimitError),
180  }
181
182  impl fmt::Display for UploadError {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184      f.write_str("upload failed")
185    }
186  }
187
188  impl error::Error for UploadError {
189    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
190      Some(match self {
191        UploadError::Permission(e) => e,
192        UploadError::Limit(e) => e,
193      })
194    }
195  }
196
197  #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
198  struct PermissionError;
199
200  impl fmt::Display for PermissionError {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202      f.write_str("permission denied")
203    }
204  }
205
206  impl error::Error for PermissionError {}
207
208  #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
209  struct LimitError;
210
211  impl fmt::Display for LimitError {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213      f.write_str("upload exceeds max limit")
214    }
215  }
216
217  impl error::Error for LimitError {}
218
219  #[test]
220  fn error_without_source() {
221    let input = PermissionError;
222    let actual: String = input.display_full().to_string();
223    let expected = String::from("permission denied");
224    assert_eq!(actual, expected);
225  }
226
227  #[test]
228  fn error_with_source() {
229    let input = UploadError::Permission(PermissionError);
230    let actual: String = input.display_full().to_string();
231    let expected = String::from("upload failed: permission denied");
232    assert_eq!(actual, expected);
233  }
234
235  #[test]
236  fn error_with_cyclic_source_chain() {
237    #[derive(Debug)]
238    struct CyclicError;
239
240    impl fmt::Display for CyclicError {
241      fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        f.write_str("cycle detected")
243      }
244    }
245
246    impl error::Error for CyclicError {
247      fn source(&self) -> Option<&(dyn error::Error + 'static)> {
248        Some(self as &dyn error::Error)
249      }
250    }
251
252    let input = CyclicError;
253    let actual: String = input.display_full().to_string();
254    let expected = format!("{}...", ["cycle detected: "; 1024].join(""));
255    assert_eq!(actual, expected);
256  }
257}