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}