scoped_error/many.rs
1// Copyright (C) 2026 Kan-Ru Chen <kanru@kanru.info>
2//
3// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5//! Error type with multiple simultaneous causes.
6//!
7//! Useful for aggregating errors from concurrent operations,
8//! validation with multiple failures, or any scenario where
9//! several things can fail independently.
10
11use std::borrow::Cow;
12use std::error::Error;
13use std::fmt::{Debug, Display};
14use std::panic::Location;
15
16use crate::ext::ErrorExt;
17
18/// An error type that can have multiple simultaneous causes.
19///
20/// Unlike [`scoped_error::Error`](crate::Error) which has a single causal chain,
21/// `Many` stores multiple independent errors. This is useful for:
22///
23/// - Parallel operations where multiple tasks may fail
24/// - Validation that collects all errors before reporting
25/// - Operations with multiple independent failure modes
26///
27/// The [`source`](Error::source) method returns the first cause for
28/// compatibility with the standard error interface. Use
29/// [`causes`](Self::causes) or the tree-formatted [`ErrorReport`](crate::ErrorReport) to
30/// access all errors.
31///
32/// # Example
33///
34/// ```
35/// use scoped_error::{Error, Many, expect_error};
36/// use std::thread;
37///
38/// fn parallel_work() -> Result<Vec<()>, Many> {
39/// let handles = vec![
40/// thread::spawn(|| task_a()),
41/// thread::spawn(|| task_b()),
42/// ];
43///
44/// let results: Vec<_> = handles
45/// .into_iter()
46/// .map(|h| h.join().unwrap())
47/// .collect();
48///
49/// Many::from_results("parallel tasks failed", results)
50/// }
51///
52/// fn task_a() -> Result<(), Error> {
53/// expect_error("task A failed", || {
54/// some_fallible_op()?;
55/// Ok(())
56/// })
57/// }
58///
59/// fn task_b() -> Result<(), Error> {
60/// expect_error("task B failed", || {
61/// some_fallible_op()?;
62/// Ok(())
63/// })
64/// }
65///
66/// # fn some_fallible_op() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Ok(()) };
67/// ```
68pub struct Many {
69 /// The primary error message describing the overall failure.
70 pub message: Cow<'static, str>,
71 /// All independent causes of this error.
72 pub causes: Vec<Box<dyn Error + Send + Sync + 'static>>,
73 /// Where this error was created.
74 pub location: Option<&'static Location<'static>>,
75}
76
77impl Many {
78 /// Create a new `Many` with the given message.
79 ///
80 /// The causes list starts empty. Use [`with_cause`](Self::with_cause)
81 /// or [`from_results`](Self::from_results) to populate it.
82 ///
83 /// # Example
84 ///
85 /// ```
86 /// use scoped_error::Many;
87 ///
88 /// let err = Many::new("validation failed");
89 /// ```
90 #[track_caller]
91 pub fn new(msg: impl Into<Cow<'static, str>>) -> Self {
92 Self {
93 message: msg.into(),
94 causes: Vec::new(),
95 location: Some(Location::caller()),
96 }
97 }
98
99 /// Add a cause to this error.
100 ///
101 /// Returns `self` for chaining.
102 ///
103 /// # Example
104 ///
105 /// ```
106 /// use scoped_error::Many;
107 ///
108 /// let err = Many::new("multiple failures")
109 /// .with_cause(std::io::Error::other("disk full"))
110 /// .with_cause(std::io::Error::other("network timeout"));
111 /// ```
112 pub fn with_cause<E>(mut self, cause: E) -> Self
113 where
114 E: Into<Box<dyn Error + Send + Sync + 'static>>,
115 {
116 self.causes.push(cause.into());
117 self
118 }
119
120 /// Get all causes as a slice.
121 ///
122 /// Use this to iterate over all errors when the tree-formatted
123 /// report doesn't provide enough control.
124 ///
125 /// # Example
126 ///
127 /// ```
128 /// use scoped_error::Many;
129 ///
130 /// let err = Many::new("example");
131 /// for (i, cause) in err.causes().iter().enumerate() {
132 /// println!("Failure {}: {}", i, cause);
133 /// }
134 /// ```
135 pub fn causes(&self) -> &[Box<dyn Error + Send + Sync + 'static>] {
136 &self.causes
137 }
138
139 /// Collect results, returning Ok if all succeeded, Err with all failures.
140 ///
141 /// This is the primary way to construct a `Many` from
142 /// multiple operations. If all results are `Ok`, returns `Ok` with
143 /// all the success values. If any are `Err`, returns `Err` with a
144 /// `Many` containing all the errors.
145 ///
146 /// # Type Parameters
147 ///
148 /// - `T`: The success type of each result
149 /// - `E`: The error type (must be convertible to the boxed error type)
150 ///
151 /// # Example
152 ///
153 /// ```
154 /// use scoped_error::Many;
155 ///
156 /// let results: Vec<Result<i32, std::io::Error>> = vec![
157 /// Ok(1),
158 /// Err(std::io::Error::other("fail 1")),
159 /// Err(std::io::Error::other("fail 2")),
160 /// ];
161 ///
162 /// match Many::from_results("batch operation failed", results) {
163 /// Ok(values) => println!("Success: {:?}", values),
164 /// Err(e) => println!("{}\nCaused by {} errors", e, e.causes().len()),
165 /// }
166 /// ```
167 #[track_caller]
168 pub fn from_results<T, E>(
169 msg: impl Into<Cow<'static, str>>,
170 results: impl IntoIterator<Item = Result<T, E>>,
171 ) -> Result<Vec<T>, Self>
172 where
173 E: Into<Box<dyn Error + Send + Sync + 'static>>,
174 {
175 let mut oks = Vec::new();
176 let mut errs = Vec::new();
177
178 for result in results {
179 match result {
180 Ok(v) => oks.push(v),
181 Err(e) => errs.push(e.into()),
182 }
183 }
184
185 if errs.is_empty() {
186 Ok(oks)
187 } else {
188 Err(Self {
189 message: msg.into(),
190 causes: errs,
191 location: Some(Location::caller()),
192 })
193 }
194 }
195
196 /// Create from an iterator of errors, with a message.
197 ///
198 /// Unlike `from_results`, this always returns `Err`. Use when you
199 /// already know you have failures to report.
200 ///
201 /// # Example
202 ///
203 /// ```
204 /// use scoped_error::Many;
205 ///
206 /// let errors: Vec<std::io::Error> = vec![
207 /// std::io::Error::other("error 1"),
208 /// std::io::Error::other("error 2"),
209 /// ];
210 ///
211 /// let err = Many::from_errors("validation failed", errors);
212 /// ```
213 #[track_caller]
214 pub fn from_errors<E>(
215 msg: impl Into<Cow<'static, str>>,
216 errors: impl IntoIterator<Item = E>,
217 ) -> Self
218 where
219 E: Into<Box<dyn Error + Send + Sync + 'static>>,
220 {
221 Self {
222 message: msg.into(),
223 causes: errors.into_iter().map(Into::into).collect(),
224 location: Some(Location::caller()),
225 }
226 }
227
228 /// Returns true if there are no causes.
229 ///
230 /// An empty `Many` is unusual but possible if constructed
231 /// directly. Operations like `from_results` never create empty errors.
232 pub fn is_empty(&self) -> bool {
233 self.causes.is_empty()
234 }
235
236 /// Returns the number of causes.
237 pub fn len(&self) -> usize {
238 self.causes.len()
239 }
240}
241
242impl Debug for Many {
243 /// Formats using the error report for human-readable output.
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 self.report().fmt(f)
246 }
247}
248
249impl Display for Many {
250 /// Displays the message with location and cause count.
251 ///
252 /// Format: `"{message}, at {location} ({n} causes)"` or
253 /// `"{message} ({n} causes)"` if location is None.
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 if let Some(loc) = self.location {
256 write!(f, "{}, at {}", self.message, loc)?;
257 } else {
258 write!(f, "{}", self.message)?;
259 }
260 if !self.causes.is_empty() {
261 write!(
262 f,
263 " ({} {})",
264 self.causes.len(),
265 if self.causes.len() == 1 {
266 "cause"
267 } else {
268 "causes"
269 }
270 )?;
271 }
272 Ok(())
273 }
274}
275
276impl Error for Many {
277 /// Returns the first cause, if any.
278 ///
279 /// This provides compatibility with the standard `Error` trait's
280 /// single-chain model. To access all causes, use [`causes`](Self::causes)
281 /// or the tree-formatted error report.
282 ///
283 /// Returns `None` if there are no causes.
284 fn source(&self) -> Option<&(dyn Error + 'static)> {
285 self.causes
286 .first()
287 .map(|e| e.as_ref() as &(dyn Error + 'static))
288 }
289}
290
291// Conversions
292
293impl<E> From<Vec<E>> for Many
294where
295 E: Into<Box<dyn Error + Send + Sync + 'static>>,
296{
297 /// Convert a vector of errors into `many`.
298 ///
299 /// The message defaults to "Multiple errors occurred". Use
300 /// [`with_message`](Self::with_message) or [`new`](Self::new) +
301 /// [`with_cause`](Self::with_cause) for custom messages.
302 #[track_caller]
303 fn from(errors: Vec<E>) -> Self {
304 Self {
305 message: Cow::Borrowed("Multiple errors occurred"),
306 causes: errors.into_iter().map(Into::into).collect(),
307 location: Some(Location::caller()),
308 }
309 }
310}
311
312// Helper method for chaining
313impl Many {
314 /// Replace the message, returning self for chaining.
315 ///
316 /// # Example
317 ///
318 /// ```
319 /// use scoped_error::Many;
320 ///
321 /// let errors: Vec<std::io::Error> = vec![/* ... */];
322 /// let err = Many::from(errors)
323 /// .with_message("custom batch operation failed");
324 /// ```
325 pub fn with_message(mut self, msg: impl Into<Cow<'static, str>>) -> Self {
326 self.message = msg.into();
327 self
328 }
329}