error_stack/ext/
tuple.rs

1use crate::Report;
2
3/// Extends tuples with error-handling capabilities.
4///
5/// This trait provides a method to collect a tuple of `Result`s into a single `Result`
6/// containing a tuple of the successful values, or an error if any of the results failed.
7///
8/// The trait is implemented for tuples of up to 16 elements.
9///
10/// # Stability
11///
12/// This trait is only available behind the `unstable` feature flag and is not covered by
13/// semver guarantees. It may change or be removed in future versions without notice.
14pub trait TryReportTupleExt<C> {
15    /// The type of the successful output, typically a tuple of the inner types of the `Result`s.
16    type Output;
17
18    /// Attempts to collect all `Result`s in the tuple into a single `Result`.
19    ///
20    /// # Errors
21    ///
22    /// If any element is `Err`, returns the first encountered `Err`, with subsequent errors
23    /// appended to it.
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// use error_stack::{Report, TryReportTupleExt};
29    ///
30    /// #[derive(Debug)]
31    /// struct CustomError;
32    ///
33    /// impl core::fmt::Display for CustomError {
34    ///     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35    ///         write!(f, "Custom error")
36    ///     }
37    /// }
38    ///
39    /// impl core::error::Error for CustomError {}
40    ///
41    /// let result1: Result<i32, Report<CustomError>> = Ok(1);
42    /// let result2: Result<&'static str, Report<CustomError>> = Ok("success");
43    /// let result3: Result<bool, Report<CustomError>> = Ok(true);
44    ///
45    /// let combined = (result1, result2, result3).try_collect();
46    /// assert_eq!(combined.unwrap(), (1, "success", true));
47    ///
48    /// let result1: Result<i32, Report<CustomError>> = Ok(1);
49    /// let result2: Result<&'static str, Report<CustomError>> = Err(Report::new(CustomError));
50    /// let result3: Result<bool, Report<CustomError>> = Err(Report::new(CustomError));
51    /// let combined_with_error = (result1, result2, result3).try_collect();
52    /// assert!(combined_with_error.is_err());
53    /// ```
54    fn try_collect(self) -> Result<Self::Output, Report<[C]>>;
55}
56
57impl<T, R, C> TryReportTupleExt<C> for (core::result::Result<T, R>,)
58where
59    R: Into<Report<[C]>>,
60{
61    type Output = (T,);
62
63    fn try_collect(self) -> Result<Self::Output, Report<[C]>> {
64        let (result,) = self;
65
66        match result {
67            Ok(value) => Ok((value,)),
68            Err(report) => Err(report.into()),
69        }
70    }
71}
72
73#[rustfmt::skip]
74macro_rules! all_the_tuples {
75    ($macro:ident) => {
76        $macro!([A, AO]);
77        $macro!([A, AO], [B, BO]);
78        $macro!([A, AO], [B, BO], [C, CO]);
79        $macro!([A, AO], [B, BO], [C, CO], [D, DO]);
80        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO]);
81        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO]);
82        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO]);
83        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO]);
84        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO], [I, IO]);
85        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO], [I, IO], [J, JO]);
86        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO], [I, IO], [J, JO], [K, KO]);
87        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO], [I, IO], [J, JO], [K, KO], [L, LO]);
88        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO], [I, IO], [J, JO], [K, KO], [L, LO], [M, MO]);
89        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO], [I, IO], [J, JO], [K, KO], [L, LO], [M, MO], [N, NO]);
90        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO], [I, IO], [J, JO], [K, KO], [L, LO], [M, MO], [N, NO], [O, OO]);
91        $macro!([A, AO], [B, BO], [C, CO], [D, DO], [E, EO], [F, FO], [G, GO], [H, HO], [I, IO], [J, JO], [K, KO], [L, LO], [M, MO], [N, NO], [O, OO], [P, PO]);
92    };
93}
94
95macro_rules! impl_ext {
96    ($([$type:ident, $output:ident]),+) => {
97        impl<$($type, $output),*, T, R, Context> TryReportTupleExt<Context> for ($($type),*, core::result::Result<T, R>)
98        where
99            R: Into<Report<[Context]>>,
100            ($($type,)*): TryReportTupleExt<Context, Output = ($($output,)*)>,
101        {
102            type Output = ($($output),*, T);
103
104            #[expect(non_snake_case, clippy::min_ident_chars)]
105            fn try_collect(self) -> Result<Self::Output, Report<[Context]>> {
106                let ($($type),*, result) = self;
107                let prefix = ($($type,)*).try_collect();
108
109                match (prefix, result) {
110                    (Ok(($($type,)*)), Ok(value)) => Ok(($($type),*, value)),
111                    (Err(report), Ok(_)) => Err(report),
112                    (Ok(_), Err(report)) => Err(report.into()),
113                    (Err(mut report), Err(error)) => {
114                        report.append(error.into());
115                        Err(report)
116                    }
117                }
118            }
119        }
120    };
121}
122
123all_the_tuples!(impl_ext);
124
125#[cfg(test)]
126mod test {
127    use alloc::{borrow::ToOwned as _, collections::BTreeSet, string::String};
128    use core::{error::Error, fmt::Display};
129
130    use super::TryReportTupleExt as _;
131    use crate::Report;
132
133    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
134    struct TestError(usize);
135
136    impl Display for TestError {
137        fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
138            fmt.write_str("TestError")
139        }
140    }
141
142    impl Error for TestError {}
143
144    #[test]
145    fn single_error() {
146        let result1: Result<i32, Report<TestError>> = Ok(1);
147        let result2: Result<String, Report<TestError>> = Ok("test".to_owned());
148        let result3: Result<bool, Report<TestError>> = Err(Report::new(TestError(0)));
149
150        let combined = (result1, result2, result3).try_collect();
151        let report = combined.expect_err("should have error");
152
153        let contexts: BTreeSet<_> = report.current_contexts().collect();
154        assert_eq!(contexts.len(), 1);
155        assert!(contexts.contains(&TestError(0)));
156    }
157
158    #[test]
159    fn no_error() {
160        let result1: Result<i32, Report<TestError>> = Ok(1);
161        let result2: Result<String, Report<TestError>> = Ok("test".to_owned());
162        let result3: Result<bool, Report<TestError>> = Ok(true);
163
164        let combined = (result1, result2, result3).try_collect();
165        let (ok1, ok2, ok3) = combined.expect("should have no error");
166
167        assert_eq!(ok1, 1);
168        assert_eq!(ok2, "test");
169        assert!(ok3);
170    }
171
172    #[test]
173    fn expanded_error() {
174        let result1: Result<i32, Report<[TestError]>> = Ok(1);
175        let result2: Result<String, Report<[TestError]>> = Ok("test".to_owned());
176        let result3: Result<bool, Report<[TestError]>> = Err(Report::new(TestError(0)).expand());
177
178        let combined = (result1, result2, result3).try_collect();
179        let report = combined.expect_err("should have error");
180
181        // order of contexts is not guaranteed
182        let contexts: BTreeSet<_> = report.current_contexts().collect();
183        assert_eq!(contexts.len(), 1);
184        assert!(contexts.contains(&TestError(0)));
185    }
186
187    #[test]
188    fn single_and_expanded_mixed() {
189        let result1: Result<i32, Report<[TestError]>> = Ok(1);
190        let result2: Result<String, Report<TestError>> = Err(Report::new(TestError(0)));
191        let result3: Result<bool, Report<[TestError]>> = Err(Report::new(TestError(1)).expand());
192
193        let combined = (result1, result2, result3).try_collect();
194        let report = combined.expect_err("should have error");
195
196        // order of contexts is not guaranteed
197        let contexts: BTreeSet<_> = report.current_contexts().collect();
198        assert_eq!(contexts.len(), 2);
199        assert!(contexts.contains(&TestError(0)));
200        assert!(contexts.contains(&TestError(1)));
201    }
202
203    #[test]
204    fn multiple_errors() {
205        let result1: Result<i32, Report<TestError>> = Err(Report::new(TestError(0)));
206        let result2: Result<String, Report<TestError>> = Ok("test".to_owned());
207        let result3: Result<bool, Report<TestError>> = Err(Report::new(TestError(1)));
208
209        let combined = (result1, result2, result3).try_collect();
210        let report = combined.expect_err("should have error");
211
212        // order of contexts is not guaranteed
213        let contexts: BTreeSet<_> = report.current_contexts().collect();
214        assert_eq!(contexts.len(), 2);
215        assert!(contexts.contains(&TestError(0)));
216        assert!(contexts.contains(&TestError(1)));
217    }
218}