Skip to main content

errgonomic/
macros.rs

1/// [`handle!`](crate::handle) is a better alternative to [`map_err`](Result::map_err) because it doesn't capture any variables from the environment if the result is [`Ok`], only when the result is [`Err`].
2/// By contrast, a closure passed to `map_err` always captures the variables from environment, regardless of whether the result is [`Ok`] or [`Err`]
3/// Use [`handle!`](crate::handle) if you need to pass owned variables to an error variant (which is returned only in case when result is [`Err`])
4/// In addition, this macro captures the original error in the `source` variable, and sets it as the `source` key of the error variant
5///
6/// Note: [`handle!`](crate::handle) assumes that your error variant is a struct variant
7#[macro_export]
8macro_rules! handle {
9    ($result:expr, $variant:ident$(,)? $($arg:ident$(: $value:expr)?),*) => {
10        match $result {
11            Ok(value) => value,
12            Err(source) => return Err($variant {
13                source: source.into(),
14                $($arg: $crate::_into!($arg$(: $value)?)),*
15            }),
16        }
17    };
18}
19
20/// See also: [`handle_opt_take!`](crate::handle_opt_take)
21#[macro_export]
22macro_rules! handle_opt {
23    ($option:expr, $variant:ident$(,)? $($arg:ident$(: $value:expr)?),*) => {
24        match $option {
25            Some(value) => value,
26            None => return Err($variant {
27                $($arg: $crate::_into!($arg$(: $value)?)),*
28            }),
29        }
30    };
31}
32
33/// This macro is an opposite of [`handle_opt!`](crate::handle_opt) - it returns an error if the option contains a `Some` variant.
34///
35/// Note that this macro calls [`Option::take`], which will leave a `None` if the option was `Some(value)`.
36/// Note that this macro has a mandatory argument `$some_value` (used in `if let Some($some_value) = $option.take()`), which will also be passed to the error enum variant.
37#[macro_export]
38macro_rules! handle_opt_take {
39    ($option:expr, $variant:ident, $some_value:ident$(,)? $($arg:ident$(: $value:expr)?),*) => {
40        if let Some($some_value) = $option.take() {
41            return Err($variant {
42                $some_value: $some_value.into(),
43                $($arg: $crate::_into!($arg$(: $value)?)),*
44            })
45        }
46    };
47}
48
49/// Returns an error when the condition is true.
50///
51/// This is useful for guard checks that should fail fast with a specific error variant.
52#[macro_export]
53macro_rules! handle_bool {
54    ($condition:expr, $variant:ident$(,)? $($arg:ident$(: $value:expr)?),*) => {
55        if $condition {
56            return Err($variant {
57                $($arg: $crate::_into!($arg$(: $value)?)),*
58            });
59        };
60    };
61}
62
63/// Collects results from an iterator, returning a variant that wraps all errors.
64///
65/// `$results` must be an `impl Iterator<Item = Result<T, E>>`.
66#[macro_export]
67macro_rules! handle_iter {
68    ($results:expr, $variant:ident$(,)? $($arg:ident$(: $value:expr)?),*) => {
69        {
70            match $crate::partition_result($results) {
71                Ok(oks) => oks,
72                Err(errors) => {
73                    return Err($variant {
74                        source: errors.into(),
75                        $($arg: $crate::_into!($arg$(: $value)?)),*
76                    });
77                }
78            }
79        }
80    };
81}
82
83/// Collects results while keeping the corresponding input items, returning `(outputs, items)` on success.
84///
85/// This macro returns a tuple because the iteration consumes items that may be needed later.
86/// If there are no errors, `items.len() == outputs.len()`.
87/// If the results iterator terminates early, the returned `items` may be shorter than the original input.
88#[macro_export]
89macro_rules! handle_iter_of_refs {
90    ($results:expr, $items:expr, $variant:ident $(, $arg:ident$(: $value:expr)?)*) => {
91        {
92            use alloc::vec::Vec;
93            let (outputs, items, errors) = core::iter::zip($results, $items).fold(
94                (Vec::new(), Vec::new(), Vec::new()),
95                |(mut outputs, mut items, mut errors), (result, item)| {
96                    match result {
97                        Ok(output) => {
98                            outputs.push(output);
99                            items.push(item);
100                        }
101                        Err(source) => {
102                            errors.push($crate::ItemError {
103                                item,
104                                source,
105                            });
106                        }
107                    }
108                    (outputs, items, errors)
109                },
110            );
111            if errors.is_empty() {
112                (outputs, items)
113            } else {
114                return Err($variant {
115                    source: errors.into(),
116                    $($arg: $crate::_into!($arg$(: $value)?)),*
117                });
118            }
119        }
120    };
121}
122
123/// Collects results from any `IntoIterator`, wrapping all errors into one variant.
124#[macro_export]
125macro_rules! handle_into_iter {
126    ($results:expr, $variant:ident $(, $arg:ident$(: $value:expr)?)*) => {
127        $crate::handle_iter!($results.into_iter(), $variant $(, $arg$(: $value)?),*)
128    };
129}
130
131/// [`handle_discard`](crate::handle_discard) should only be used when you want to discard the source error. This is discouraged. Prefer other handle-family macros that preserve the source error.
132#[macro_export]
133macro_rules! handle_discard {
134    ($result:expr, $variant:ident$(,)? $($arg:ident$(: $value:expr)?),*) => {
135        match $result {
136            Ok(value) => value,
137            Err(_) => return Err($variant {
138                $($arg: $crate::_into!($arg$(: $value)?)),*
139            }),
140        }
141    };
142}
143
144/// [`map_err`](crate::map_err) should be used only when the error variant doesn't capture any owned variables (which is very rare), or exactly at the end of the block (in the position of returned expression).
145#[macro_export]
146macro_rules! map_err {
147    ($result:expr, $variant:ident$(,)? $($arg:ident$(: $value:expr)?),*) => {
148        $result.map_err(|source| $variant {
149            source: source.into(),
150            $($arg: $crate::_into!($arg$(: $value)?)),*
151        })
152    };
153}
154
155/// Internal
156#[doc(hidden)]
157#[macro_export]
158macro_rules! _into {
159    ($arg:ident) => {
160        $arg.into()
161    };
162    ($arg:ident: $value:expr) => {
163        $value.into()
164    };
165}
166
167/// Internal
168#[doc(hidden)]
169#[macro_export]
170macro_rules! _index_err {
171    ($f:ident) => {
172        |(index, item)| $f(item).map_err(|err| (index, err))
173    };
174}
175
176/// Internal
177#[doc(hidden)]
178#[macro_export]
179macro_rules! _index_err_async {
180    ($f:ident) => {
181        async |(index, item)| $f(item).await.map_err(|err| (index, err))
182    };
183}
184
185#[cfg(all(test, feature = "std"))]
186mod tests {
187    use crate::{ErrVec, ItemError, PathBufDisplay};
188    use futures::future::join_all;
189    use serde::{Deserialize, Serialize};
190    use std::io;
191    use std::path::{Path, PathBuf};
192    use std::str::FromStr;
193    use std::sync::{Arc, RwLock};
194    use thiserror::Error;
195    use tokio::fs::read_to_string;
196    use tokio::task::JoinSet;
197
198    #[allow(dead_code)]
199    struct PrintNameCommand {
200        dir: PathBuf,
201        format: Format,
202    }
203
204    #[allow(dead_code)]
205    impl PrintNameCommand {
206        async fn run(self) -> Result<(), PrintNameCommandError> {
207            use PrintNameCommandError::*;
208            let Self {
209                dir,
210                format,
211            } = self;
212            let config = handle!(parse_config(&dir, format).await, ParseConfigFailed);
213            println!("{}", config.name);
214            Ok(())
215        }
216    }
217
218    /// This function tests the [`crate::handle!`] macro
219    #[allow(dead_code)]
220    async fn parse_config(dir: &Path, format: Format) -> Result<Config, ParseConfigError> {
221        use Format::*;
222        use ParseConfigError::*;
223        let path_buf = dir.join("config.json");
224        let contents = handle!(read_to_string(&path_buf).await, ReadFileFailed, path: path_buf);
225        match format {
226            Json => {
227                let config = handle!(serde_json::de::from_str(&contents), DeserializeFromJson, path: path_buf, contents);
228                Ok(config)
229            }
230            Toml => {
231                let config = handle!(toml::de::from_str(&contents), DeserializeFromToml, path: path_buf, contents);
232                Ok(config)
233            }
234        }
235    }
236
237    /// This function tests the [`crate::handle_opt!`] macro
238    #[allow(dead_code)]
239    fn find_even(numbers: Vec<u32>) -> Result<u32, FindEvenError> {
240        use FindEvenError::*;
241        let even = handle_opt!(numbers.iter().find(|x| *x % 2 == 0), NotFound);
242        Ok(*even)
243    }
244
245    /// This function tests the [`crate::handle_iter!`] macro
246    #[allow(dead_code)]
247    fn multiply_evens(numbers: Vec<u32>) -> Result<Vec<u32>, MultiplyEvensError> {
248        use MultiplyEvensError::*;
249        let results = numbers.into_iter().map(|number| {
250            use CheckEvenError::*;
251            if number % 2 == 0 {
252                Ok(number * 10)
253            } else {
254                Err(NumberNotEven {
255                    number,
256                })
257            }
258        });
259        Ok(handle_iter!(results, CheckEvensFailed))
260    }
261
262    /// This function tests the [`crate::handle_into_iter!`] macro
263    #[allow(dead_code)]
264    async fn read_files(paths: Vec<PathBuf>) -> Result<Vec<String>, ReadFilesError> {
265        use ReadFilesError::*;
266        let results = paths
267            .into_iter()
268            .map(check_file)
269            .collect::<JoinSet<_>>()
270            .join_all()
271            .await;
272        Ok(handle_into_iter!(results, CheckFilesFailed))
273    }
274
275    #[allow(dead_code)]
276    async fn read_files_ref(paths: Vec<PathBuf>) -> Result<Vec<String>, ReadFilesRefError> {
277        use ReadFilesRefError::*;
278        let iter = paths.iter().map(check_file_ref);
279        let results = join_all(iter).await;
280        let items = paths.into_iter().map(PathBufDisplay::from);
281        let (outputs, _items) = handle_iter_of_refs!(results.into_iter(), items, CheckFilesRefFailed);
282        Ok(outputs)
283    }
284
285    // async fn check_file(path: &Path)
286
287    /// This function exists to test error handling in async code
288    #[allow(dead_code)]
289    async fn process(number: u32) -> Result<u32, ProcessError> {
290        Ok(number)
291    }
292
293    #[derive(Error, Debug)]
294    enum PrintNameCommandError {
295        #[error("failed to parse config")]
296        ParseConfigFailed { source: ParseConfigError },
297    }
298
299    /// Variants don't have the `format` field because every variant already corresponds to a single specific format
300    /// Some variants have the `path` field because the `contents` depends on `path`
301    /// Some `source` field types are wrapped in `Box` according to suggestion from `result_large_err` lint
302    #[derive(Error, Debug)]
303    enum ParseConfigError {
304        #[error("failed to read the file: {path}")]
305        ReadFileFailed { path: PathBuf, source: io::Error },
306        #[error("failed to deserialize the file contents from JSON: {path}")]
307        DeserializeFromJson { path: PathBuf, contents: String, source: Box<serde_json::error::Error> },
308        #[error("failed to deserialize the file contents from TOML: {path}")]
309        DeserializeFromToml { path: PathBuf, contents: String, source: Box<toml::de::Error> },
310    }
311
312    #[allow(dead_code)]
313    #[derive(Error, Debug)]
314    enum ProcessError {}
315
316    #[allow(dead_code)]
317    #[derive(Copy, Clone, Debug)]
318    enum Format {
319        Json,
320        Toml,
321    }
322
323    #[derive(Serialize, Deserialize, Clone, Debug)]
324    struct Config {
325        name: String,
326        timeout: u64,
327        parallel: bool,
328    }
329
330    #[allow(dead_code)]
331    fn parse_even_number(input: &str) -> Result<u32, ParseEvenNumberError> {
332        use ParseEvenNumberError::*;
333        let number = handle!(input.parse::<u32>(), InputParseFailed);
334        handle_bool!(number % 2 != 0, NumberNotEven, number);
335        Ok(number)
336    }
337
338    #[derive(Error, Debug)]
339    enum ParseEvenNumberError {
340        #[error("failed to parse input")]
341        InputParseFailed { source: <u32 as FromStr>::Err },
342        #[error("number is not even: {number}")]
343        NumberNotEven { number: u32 },
344    }
345
346    #[derive(Error, Debug)]
347    enum FindEvenError {
348        #[error("even number not found")]
349        NotFound,
350    }
351
352    #[derive(Error, Debug)]
353    enum MultiplyEvensError {
354        #[error("failed to check {len} numbers", len = source.len())]
355        CheckEvensFailed { source: ErrVec<CheckEvenError> },
356    }
357
358    #[derive(Error, Debug)]
359    enum ReadFilesError {
360        #[error("failed to check {len} files", len = source.len())]
361        CheckFilesFailed { source: ErrVec<CheckFileError> },
362    }
363
364    #[derive(Error, Debug)]
365    enum ReadFilesRefError {
366        #[error("failed to check {len} files", len = source.len())]
367        CheckFilesRefFailed { source: ErrVec<ItemError<PathBufDisplay, CheckFileRefError>> },
368    }
369
370    #[derive(Error, Debug)]
371    enum CheckEvenError {
372        #[error("number is not even: {number}")]
373        NumberNotEven { number: u32 },
374    }
375
376    async fn check_file(path: PathBuf) -> Result<String, CheckFileError> {
377        use CheckFileError::*;
378        let content = handle!(read_to_string(&path).await, ReadToStringFailed, path);
379        handle_bool!(content.is_empty(), FileIsEmpty, path);
380        Ok(content)
381    }
382
383    #[derive(Error, Debug)]
384    enum CheckFileError {
385        #[error("failed to read the file to string: {path}")]
386        ReadToStringFailed { path: PathBuf, source: io::Error },
387        #[error("file is empty: {path}")]
388        FileIsEmpty { path: PathBuf },
389    }
390
391    async fn check_file_ref(path: &PathBuf) -> Result<String, CheckFileRefError> {
392        use CheckFileRefError::*;
393        let content = handle!(read_to_string(&path).await, ReadToStringFailed);
394        handle_bool!(content.is_empty(), FileIsEmpty);
395        Ok(content)
396    }
397
398    #[derive(Error, Debug)]
399    enum CheckFileRefError {
400        #[error("failed to read the file to string")]
401        ReadToStringFailed { source: io::Error },
402        #[error("file is empty")]
403        FileIsEmpty,
404    }
405
406    #[derive(Clone, Debug)]
407    struct Db {
408        user: User,
409    }
410
411    #[derive(Clone, Debug)]
412    struct User {
413        username: String,
414    }
415
416    #[allow(dead_code)]
417    fn get_username(db: Arc<RwLock<Db>>) -> Result<String, GetUsernameError> {
418        use GetUsernameError::*;
419        // `db.read()` returns `LockResult` whose Err variant is `PoisonError<RwLockReadGuard<'_, T>>`, which contains an anonymous lifetime
420        // The error enum returned from this function must contain only owned fields, so it can't contain a `source` that has a lifetime
421        // Therefore, we have to use handle_discard!, although it is discouraged
422        let guard = handle_discard!(db.read(), AcquireReadLockFailed);
423        let username = guard.user.username.clone();
424        Ok(username)
425    }
426
427    #[derive(Error, Debug)]
428    pub enum GetUsernameError {
429        #[error("failed to acquire read lock")]
430        AcquireReadLockFailed,
431    }
432
433    #[allow(dead_code)]
434    fn get_answer(prompt: String, get_response: &mut impl FnMut(String) -> Result<WeirdResponse, io::Error>) -> Result<String, GetAnswerError> {
435        use GetAnswerError::*;
436        // Since the `get_response` external API doesn't return the `prompt` in its error, we have to clone `prompt` before passing it as argument, so that we could pass it to the error enum variant
437        // Cloning may be necessary with external APIs that don't return arguments in errors, but it must not be necessary in our code
438        let mut response = handle!(get_response(prompt.clone()), GetResponseFailed, prompt);
439        handle_opt_take!(response.error, ResponseContainsError, error);
440        Ok(response.answer)
441    }
442
443    /// OpenAI Responses API returns a response with `error: Option<WeirdResponseError>` field, which is weird, but must still be handled
444    #[derive(Debug)]
445    pub struct WeirdResponse {
446        answer: String,
447        error: Option<WeirdResponseError>,
448    }
449
450    #[allow(dead_code)]
451    #[derive(Error, Debug)]
452    pub enum WeirdResponseError {
453        #[error("prompt is empty")]
454        PromptIsEmpty,
455        #[error("context limit reached")]
456        ContextLimitReached,
457    }
458
459    /// [`GetAnswerError::GetResponseFailed`] `error` attribute doesn't contain a reference to `{prompt}` because the prompt can be very long, so it would make the error message very long, which is undesirable
460    #[derive(Error, Debug)]
461    pub enum GetAnswerError {
462        #[error("failed to get response")]
463        GetResponseFailed { source: io::Error, prompt: String },
464        #[error("response contains an error")]
465        ResponseContainsError { error: WeirdResponseError },
466    }
467}