When is mapping better for error handling than immediate ```match``` or propagation?

Hello everyone!

In Rust, the core::result::Result enum is used for reporting and handling errors. Typically, when the fallible function returns an error, its caller propagates it further with ? operator (if it doesn't have enough context to handle the error) or uses match of if let to handle the error (similar to exceptions in other languages). However, Rust also has the alternative -- map(), and_then() and or_else() methods on Result objects (and similar), in which the fallible process can be split into parts, and the next part is called only if the previous one had succeeded. It can be used in command-line utilities or fallible network or IPC request handlers, when successes and failures are reported to some external destination, and the code looks more clean to me with mapping than with matching (and returning on failure) on each fallible function call. I wonder, in which cases is it better to handle errors with immediate matching, and in which cases mapping is better?

P.S. I also wonder, if a function accepts a closure through a generic type implementing FnOnce trait, can such closure be inlined?

Mapping is pattern matching: result.rs - source

Sorry if I used incorrect terms, I mean matching by the caller immediately after return (as far as I understand, Result values always have to be pattern-matched to distinguish between success and failure, the difference is only where it's performed).

When a function returns a Result, the returned result has always to be handled by the caller of that function, since this is, where the Result is returned.
Maybe an exemplary code snipped could clarify what your question is.

This is mostly a matter of convenience. Its important to keep in mind that even if you use the ? symbol, the error you're "propagating" still needs to be of the same type as you declared in your function signature, thins like map and and_then are just a way to conveniently get an error and transform it into another type of error that your function is expected to return.

While I almost always use map, one technical caveat is that since it uses a closure, sometimes you might run into lifetime restrictions, so I use a match or if let in such cases.

1 Like

In cases where the same effect could be written with either style, you should write whichever one is clearer to read.

In my own personal opinion, .map() or .and_then() are rarely the best thing to use, but that is not saying that they are bad; rather, that the exact situations where they are the right choice rarely comes up, and other options may be equally good then. In particular, the ? operator is often an alternative. Compare these three pieces of code (suppose that in all cases, they return a value from the current function):

match something() {
    Ok(x) => Ok(vec![x]),
    Err(e) => Err(e),
}
something().map(|x| vec![x])
Ok(vec![something()?])

In this case, they are overall quite similar, but the match has more boilerplate, so either of the others would typically be better. But now suppose that we want to put two things in the vector:

match something() {
    Ok(x) => match something_else() {
        Ok(y) => Ok(vec![x, y]),
        Err(e) => Err(e),
    },
    Err(e) => Err(e),
}
something().and_then(|x| something_else().map(|y| vec![x, y]))
Ok(vec![something()?, something_else()?])

The ? version clearly now wins in terms of simplicity. Thus, I generally prefer to write it for consistency; code is easier to read if it uses one technique (here ?) everywhere it can be used rather than a mix of different styles.

If the Result were not being returned from the function, then ? would not be an option, and the .map() or .and_then() might be cleanest. But I find that long method chains and nested higher-order functions aren’t the most readable, because they require the reader to keep track of all the unwritten types involve as they read the code. In complex cases, I'd often rather use a match because it explicitly lays out all the cases that can occur; for example, if something_else() doesn't have any side effects or large costs, so executing it spuriously is not a concern, I might prefer:

match (something(), something_else()) {
    (Ok(x), Ok(y)) => Ok(vec![x, y]),
    (Err(e), _) | (_, Err(e)) => Err(e),
}

In summary: Write what makes the code easy to read. Conciseness, consistency, and explicitness are all factors to consider, and often in tension with each other, so look at your particular situation and make what you think is the best choice when writing each piece of code — but in context of the rest of the code.

2 Likes

Here's a somewhat long-winded example of using both map_err and ?, including the ownership/capturing problems that closures can introduce.

I do a lot of map_err mapping to add context:

struct Error { path: Box<Path>, kind: ErrorKind, }
enum ErrorKind {
    Open(io::Error),
    Read(io::Error),
    Parse(ParseError),
}

impl<P: Into<Box<Path>>> From<(P, ErrorKind)> for Error { ... }

fn example(path: &Path) -> Result<(), Error> {
    let file = File::open(path).map_err(|e| (path, ErrorKind::Open(e)))?;
    do_stuff(file).map_err(|e| (path, e))?;
    Ok(())
}

fn do_stuff(file: File) -> Result<(), ErrorKind> { ... }

Here we're using both map_err and ? to construct better errors. With I/O in particular, errors can arise many tiimes in a single function, and it can be quite cumbersome to have matches and struct construction expressions all over the place.

    let file = match File::open(path) {
        Ok(file) => file,
        Err(e) => return Err(Error {
            path: path.into(),
            kind: ErrorKind::Open(e),
        }),
    };

Now, in this scenario, you can easily run into ownership conflicts:

fn other_example(mut path: PathBuf) -> Result<PathBuf, Error> {
    // edit the path or whatever, then
    let file = File::open(&*path).map_err(|e| (path, ErrorKind::Open(e)))?;
    do_stuff(file).map_err(|e| (path, e))?;
    Ok(path)
}
error[E0382]: use of moved value: `path`
  --> src/lib.rs:35:28
   |
33 | fn other_example(mut path: PathBuf) -> Result<PathBuf, Error> {
   |                  -------- move occurs because `path` has type `PathBuf`, which does not implement the `Copy` trait
34 |     let file = File::open(&*path).map_err(|e| (path, ErrorKind::Open(e)))?;
   |                                           ---  ---- variable moved due to use in closure
   |                                           |
   |                                           value moved into closure here
35 |     do_stuff(file).map_err(|e| (path, e))?;
   |                            ^^^  ---- use occurs due to use in closure
   |                            |
   |                            value used here after move

You could just eat the cost of a clone here:

    // the `From` impl will effectively clone the `PathBuf` now
    do_stuff(file).map_err(|e| (&*path, e))?;

And I don't really mind this at all, as it is the error path. However, an alternative is to avoid the closure here and match.

fn other_example(path: PathBuf) -> Result<PathBuf, Error> {
    let file = match File::open(&*path).map_err(ErrorKind::Open) {
        Ok(file) => file,
        Err(e) => return Err((path, e).into()),
    };
    match do_stuff(file) {
        Ok(()) => Ok(path),
        Err(e) => Err((path, e).into()),
    }
}

And this comes up with map, or_else, and other similar closure-utilizing methods too, it's not an error handling specific situation.

Note how using some mapping (and sometimes ?), etc, can still avoid some of the verbosity.

Yes, closures can get inlined.

But note that the capturing of variables used to create the closure cannot be inlined away, in the sense that the ownership example above will start compiling. If creating and passing the closure somewhere is unconditional, so is the capturing.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.