Skip to main content

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}