error2/backtrace/mod.rs
1mod double_locations;
2mod message;
3
4use std::{any, error::Error, fmt, mem};
5
6use self::{double_locations::DoubleLocations, message::Message};
7use crate::Location;
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub(crate) enum BakctraceEntry {
12 Message(Message),
13 Locations(DoubleLocations),
14}
15
16const _: () = {
17 ["Size of `Message`"][mem::size_of::<Message>() - 24usize];
18 ["Size of `DoubleLocations`"][mem::size_of::<DoubleLocations>() - 24usize];
19 ["`Message` and `DoubleLocations` must have the same size"]
20 [mem::size_of::<Message>() - mem::size_of::<DoubleLocations>()];
21 ["Size of `BakctraceEntry`"][mem::size_of::<BakctraceEntry>() - 32usize];
22};
23
24/// A backtrace that tracks error propagation through the call stack.
25///
26/// `Backtrace` stores a complete history of an error's journey, including:
27/// - Error messages and type names
28/// - Source code locations (file:line:column)
29/// - Nested error chains
30///
31/// # Overview
32///
33/// Unlike `std::backtrace::Backtrace` which captures the call stack,
34/// `error2::Backtrace` tracks the logical error propagation path through
35/// your application, showing where errors were created, converted, and propagated.
36///
37/// # Creation
38///
39/// Backtraces are typically created automatically by the `#[derive(Error2)]` macro:
40///
41/// ```
42/// use error2::prelude::*;
43///
44/// #[derive(Debug, Error2)]
45/// #[error2(display("my error"))]
46/// struct MyError {
47/// backtrace: Backtrace,
48/// }
49/// ```
50///
51/// # Accessing Error Information
52///
53/// Use `.error_message()` to get a formatted error chain:
54///
55/// ```
56/// # use error2::prelude::*;
57/// # use std::io;
58/// # #[derive(Debug, Error2)]
59/// # #[error2(display("test error"))]
60/// # struct TestError { source: io::Error, backtrace: Backtrace }
61/// # fn operation() -> Result<(), TestError> {
62/// # let err = io::Error::new(io::ErrorKind::NotFound, "file not found");
63/// # Err(err).context(TestError2)
64/// # }
65/// use regex::Regex;
66///
67/// if let Err(e) = operation() {
68/// let msg = e.backtrace().error_message();
69///
70/// // Full error format with location tracking:
71/// // TestError: test error
72/// // at /path/to/file.rs:128:14
73/// // std::io::error::Error: file not found
74///
75/// let re = Regex::new(concat!(
76/// r"(?s)^.+TestError: test error",
77/// r"\n at .+\.rs:\d+:\d+",
78/// r"\nstd::io::error::Error: file not found$",
79/// ))
80/// .unwrap();
81/// assert!(re.is_match(msg.as_ref()));
82/// }
83/// ```
84///
85/// # Location Tracking
86///
87/// The backtrace automatically records the location where an error is first created.
88/// To track propagation through your call stack, you must manually call:
89/// - `.attach()` - Records the current location when an error passes through
90/// - `.attach_location()` - Records a specific location
91///
92/// When converting errors:
93/// - `.context()` automatically captures where the conversion happens
94/// - `.build()` / `.fail()` automatically captures where the root error is created
95///
96/// These methods use `#[track_caller]` to capture the caller's location without manual intervention.
97///
98/// # Example with Nested Errors
99///
100/// ```
101/// use std::io;
102///
103/// use error2::prelude::*;
104/// use regex::Regex;
105///
106/// #[derive(Debug, Error2)]
107/// pub enum ConfigError {
108/// #[error2(display("Failed to read: {path}"))]
109/// Read {
110/// path: String,
111/// source: io::Error,
112/// backtrace: Backtrace,
113/// },
114/// }
115///
116/// #[derive(Debug, Error2)]
117/// pub enum AppError {
118/// #[error2(display("Config error"))]
119/// Config { source: ConfigError },
120/// }
121///
122/// fn read_config() -> Result<String, ConfigError> {
123/// std::fs::read_to_string("config.toml").context(Read2 {
124/// path: "config.toml",
125/// })
126/// }
127///
128/// fn start_app() -> Result<String, AppError> {
129/// read_config().context(Config2)
130/// }
131///
132/// if let Err(e) = start_app() {
133/// let msg = e.backtrace().error_message();
134///
135/// // Full error chain format:
136/// // AppError: Config error
137/// // at /path/to/file.rs:184:19
138/// // ConfigError: Failed to read: config.toml
139/// // at /path/to/file.rs:178:44
140/// // std::io::error::Error: No such file or directory (os error 2)
141///
142/// let re = Regex::new(concat!(
143/// r"(?s)^.+AppError: Config error",
144/// r"\n at .+\.rs:\d+:\d+",
145/// r"\n.+ConfigError: Failed to read: config\.toml",
146/// r"\n at .+\.rs:\d+:\d+",
147/// r"\nstd::io::error::Error: No such file or directory \(os error 2\)$",
148/// ))
149/// .unwrap();
150/// assert!(re.is_match(msg.as_ref()));
151/// }
152/// ```
153#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
155pub struct Backtrace {
156 entries: Vec<BakctraceEntry>,
157}
158
159impl fmt::Debug for Backtrace {
160 #[inline]
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 write!(f, "Backtrace {{ ... }}")
163 }
164}
165
166impl Backtrace {
167 #[doc(hidden)]
168 #[inline]
169 pub const fn new() -> Self {
170 Self {
171 entries: Vec::new(),
172 }
173 }
174
175 #[doc(hidden)]
176 pub fn with_head<E: Error>(source: &E) -> Backtrace {
177 fn inner(type_name: &'static str, display: String) -> Backtrace {
178 Backtrace {
179 entries: vec![BakctraceEntry::Message(Message::new(type_name, display))],
180 }
181 }
182
183 let type_name = any::type_name::<E>();
184 let display = source.to_string();
185
186 inner(type_name, display)
187 }
188
189 pub(crate) fn push_error(
190 &mut self,
191 type_name: &'static str,
192 display: String,
193 location: Location,
194 ) {
195 self.entries
196 .push(BakctraceEntry::Message(Message::new(type_name, display)));
197
198 self.entries
199 .push(BakctraceEntry::Locations(DoubleLocations::new(location)));
200 }
201
202 pub(crate) const fn head_and_entries(&self) -> (Option<&Message>, &[BakctraceEntry]) {
203 let entries = self.entries.as_slice();
204
205 match entries {
206 [] => (None, &[]),
207 [BakctraceEntry::Locations(_), ..] => unreachable!(),
208 [BakctraceEntry::Message(first), rest @ ..] => match rest {
209 [] => (Some(first), &[]),
210 [BakctraceEntry::Message(_second), ..] => (Some(first), rest),
211 [BakctraceEntry::Locations(_second), ..] => (None, entries),
212 },
213 }
214 }
215
216 pub(crate) fn push_location(&mut self, location: Location) {
217 debug_assert!(matches!(
218 self.entries.first(),
219 Some(BakctraceEntry::Message(_))
220 ));
221
222 let entry = self
223 .entries
224 .last_mut()
225 .expect("there is must at least one message entry");
226
227 match entry {
228 BakctraceEntry::Locations(locations) if !locations.is_full() => {
229 let l = locations.push(location);
230 debug_assert!(l.is_none());
231 }
232 BakctraceEntry::Message(_) | BakctraceEntry::Locations(_) => {
233 self.entries
234 .push(BakctraceEntry::Locations(DoubleLocations::new(location)));
235 }
236 }
237 }
238
239 /// Returns a formatted string containing the complete error chain.
240 ///
241 /// This method produces a human-readable representation of the entire error
242 /// history, including all nested errors and their locations.
243 ///
244 /// # Examples
245 ///
246 /// ```
247 /// # use error2::prelude::*;
248 /// # use std::io;
249 /// # #[derive(Debug, Error2)]
250 /// # #[error2(display("test error"))]
251 /// # struct TestError { source: io::Error, backtrace: Backtrace }
252 /// # fn operation() -> Result<(), TestError> {
253 /// # let err = io::Error::new(io::ErrorKind::NotFound, "file not found");
254 /// # Err(err).context(TestError2)
255 /// # }
256 /// use regex::Regex;
257 ///
258 /// if let Err(e) = operation() {
259 /// let msg = e.backtrace().error_message();
260 ///
261 /// // Full error format with location tracking:
262 /// // TestError: test error
263 /// // at /path/to/file.rs:197:14
264 /// // std::io::error::Error: file not found
265 ///
266 /// let re = Regex::new(concat!(
267 /// r"(?s)^.+TestError: test error",
268 /// r"\n at .+\.rs:\d+:\d+",
269 /// r"\nstd::io::error::Error: file not found$",
270 /// ))
271 /// .unwrap();
272 /// assert!(re.is_match(msg.as_ref()));
273 /// }
274 /// ```
275 pub fn error_message(&self) -> Box<str> {
276 crate::extract_error_message(self)
277 }
278}