scoped-error 0.1.4

Structured error handling with semantic context trees.
Documentation
// Copyright (C) 2026 Kan-Ru Chen <kanru@kanru.info>
//
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

//! Error type with multiple simultaneous causes.
//!
//! Useful for aggregating errors from concurrent operations,
//! validation with multiple failures, or any scenario where
//! several things can fail independently.

use std::borrow::Cow;
use std::error::Error;
use std::fmt::{Debug, Display};
use std::panic::Location;

use crate::ext::ErrorExt;

/// An error type that can have multiple simultaneous causes.
///
/// Unlike [`scoped_error::Error`](crate::Error) which has a single causal chain,
/// `Many` stores multiple independent errors. This is useful for:
///
/// - Parallel operations where multiple tasks may fail
/// - Validation that collects all errors before reporting
/// - Operations with multiple independent failure modes
///
/// The [`source`](Error::source) method returns the first cause for
/// compatibility with the standard error interface. Use
/// [`causes`](Self::causes) or the tree-formatted [`ErrorReport`](crate::ErrorReport) to
/// access all errors.
///
/// # Example
///
/// ```
/// use scoped_error::{Error, Many, expect_error};
/// use std::thread;
///
/// fn parallel_work() -> Result<Vec<()>, Many> {
///     let handles = vec![
///         thread::spawn(|| task_a()),
///         thread::spawn(|| task_b()),
///     ];
///     
///     let results: Vec<_> = handles
///         .into_iter()
///         .map(|h| h.join().unwrap())
///         .collect();
///     
///     Many::from_results("parallel tasks failed", results)
/// }
///
/// fn task_a() -> Result<(), Error> {
///     expect_error("task A failed", || {
///         some_fallible_op()?;
///         Ok(())
///     })
/// }
///
/// fn task_b() -> Result<(), Error> {
///     expect_error("task B failed", || {
///         some_fallible_op()?;
///         Ok(())
///     })
/// }
///
/// # fn some_fallible_op() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Ok(()) };
/// ```
pub struct Many {
    /// The primary error message describing the overall failure.
    pub message: Cow<'static, str>,
    /// All independent causes of this error.
    pub causes: Vec<Box<dyn Error + Send + Sync + 'static>>,
    /// Where this error was created.
    pub location: Option<&'static Location<'static>>,
}

impl Many {
    /// Create a new `Many` with the given message.
    ///
    /// The causes list starts empty. Use [`with_cause`](Self::with_cause)
    /// or [`from_results`](Self::from_results) to populate it.
    ///
    /// # Example
    ///
    /// ```
    /// use scoped_error::Many;
    ///
    /// let err = Many::new("validation failed");
    /// ```
    #[track_caller]
    pub fn new(msg: impl Into<Cow<'static, str>>) -> Self {
        Self {
            message: msg.into(),
            causes: Vec::new(),
            location: Some(Location::caller()),
        }
    }

    /// Add a cause to this error.
    ///
    /// Returns `self` for chaining.
    ///
    /// # Example
    ///
    /// ```
    /// use scoped_error::Many;
    ///
    /// let err = Many::new("multiple failures")
    ///     .with_cause(std::io::Error::other("disk full"))
    ///     .with_cause(std::io::Error::other("network timeout"));
    /// ```
    pub fn with_cause<E>(mut self, cause: E) -> Self
    where
        E: Into<Box<dyn Error + Send + Sync + 'static>>,
    {
        self.causes.push(cause.into());
        self
    }

    /// Get all causes as a slice.
    ///
    /// Use this to iterate over all errors when the tree-formatted
    /// report doesn't provide enough control.
    ///
    /// # Example
    ///
    /// ```
    /// use scoped_error::Many;
    ///
    /// let err = Many::new("example");
    /// for (i, cause) in err.causes().iter().enumerate() {
    ///     println!("Failure {}: {}", i, cause);
    /// }
    /// ```
    pub fn causes(&self) -> &[Box<dyn Error + Send + Sync + 'static>] {
        &self.causes
    }

    /// Collect results, returning Ok if all succeeded, Err with all failures.
    ///
    /// This is the primary way to construct a `Many` from
    /// multiple operations. If all results are `Ok`, returns `Ok` with
    /// all the success values. If any are `Err`, returns `Err` with a
    /// `Many` containing all the errors.
    ///
    /// # Type Parameters
    ///
    /// - `T`: The success type of each result
    /// - `E`: The error type (must be convertible to the boxed error type)
    ///
    /// # Example
    ///
    /// ```
    /// use scoped_error::Many;
    ///
    /// let results: Vec<Result<i32, std::io::Error>> = vec![
    ///     Ok(1),
    ///     Err(std::io::Error::other("fail 1")),
    ///     Err(std::io::Error::other("fail 2")),
    /// ];
    ///
    /// match Many::from_results("batch operation failed", results) {
    ///     Ok(values) => println!("Success: {:?}", values),
    ///     Err(e) => println!("{}\nCaused by {} errors", e, e.causes().len()),
    /// }
    /// ```
    #[track_caller]
    pub fn from_results<T, E>(
        msg: impl Into<Cow<'static, str>>,
        results: impl IntoIterator<Item = Result<T, E>>,
    ) -> Result<Vec<T>, Self>
    where
        E: Into<Box<dyn Error + Send + Sync + 'static>>,
    {
        let mut oks = Vec::new();
        let mut errs = Vec::new();

        for result in results {
            match result {
                Ok(v) => oks.push(v),
                Err(e) => errs.push(e.into()),
            }
        }

        if errs.is_empty() {
            Ok(oks)
        } else {
            Err(Self {
                message: msg.into(),
                causes: errs,
                location: Some(Location::caller()),
            })
        }
    }

    /// Create from an iterator of errors, with a message.
    ///
    /// Unlike `from_results`, this always returns `Err`. Use when you
    /// already know you have failures to report.
    ///
    /// # Example
    ///
    /// ```
    /// use scoped_error::Many;
    ///
    /// let errors: Vec<std::io::Error> = vec![
    ///     std::io::Error::other("error 1"),
    ///     std::io::Error::other("error 2"),
    /// ];
    ///
    /// let err = Many::from_errors("validation failed", errors);
    /// ```
    #[track_caller]
    pub fn from_errors<E>(
        msg: impl Into<Cow<'static, str>>,
        errors: impl IntoIterator<Item = E>,
    ) -> Self
    where
        E: Into<Box<dyn Error + Send + Sync + 'static>>,
    {
        Self {
            message: msg.into(),
            causes: errors.into_iter().map(Into::into).collect(),
            location: Some(Location::caller()),
        }
    }

    /// Returns true if there are no causes.
    ///
    /// An empty `Many` is unusual but possible if constructed
    /// directly. Operations like `from_results` never create empty errors.
    pub fn is_empty(&self) -> bool {
        self.causes.is_empty()
    }

    /// Returns the number of causes.
    pub fn len(&self) -> usize {
        self.causes.len()
    }
}

impl Debug for Many {
    /// Formats using the error report for human-readable output.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.report().fmt(f)
    }
}

impl Display for Many {
    /// Displays the message with location and cause count.
    ///
    /// Format: `"{message}, at {location} ({n} causes)"` or
    /// `"{message} ({n} causes)"` if location is None.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(loc) = self.location {
            write!(f, "{}, at {}", self.message, loc)?;
        } else {
            write!(f, "{}", self.message)?;
        }
        if !self.causes.is_empty() {
            write!(
                f,
                " ({} {})",
                self.causes.len(),
                if self.causes.len() == 1 {
                    "cause"
                } else {
                    "causes"
                }
            )?;
        }
        Ok(())
    }
}

impl Error for Many {
    /// Returns the first cause, if any.
    ///
    /// This provides compatibility with the standard `Error` trait's
    /// single-chain model. To access all causes, use [`causes`](Self::causes)
    /// or the tree-formatted error report.
    ///
    /// Returns `None` if there are no causes.
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.causes
            .first()
            .map(|e| e.as_ref() as &(dyn Error + 'static))
    }
}

// Conversions

impl<E> From<Vec<E>> for Many
where
    E: Into<Box<dyn Error + Send + Sync + 'static>>,
{
    /// Convert a vector of errors into `many`.
    ///
    /// The message defaults to "Multiple errors occurred". Use
    /// [`with_message`](Self::with_message) or [`new`](Self::new) +
    /// [`with_cause`](Self::with_cause) for custom messages.
    #[track_caller]
    fn from(errors: Vec<E>) -> Self {
        Self {
            message: Cow::Borrowed("Multiple errors occurred"),
            causes: errors.into_iter().map(Into::into).collect(),
            location: Some(Location::caller()),
        }
    }
}

// Helper method for chaining
impl Many {
    /// Replace the message, returning self for chaining.
    ///
    /// # Example
    ///
    /// ```
    /// use scoped_error::Many;
    ///
    /// let errors: Vec<std::io::Error> = vec![/* ... */];
    /// let err = Many::from(errors)
    ///     .with_message("custom batch operation failed");
    /// ```
    pub fn with_message(mut self, msg: impl Into<Cow<'static, str>>) -> Self {
        self.message = msg.into();
        self
    }
}