Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the idiomatic way to handle/unwrap nested Result types?

I read that using unwrap on a Result is not a good practice in Rust and that it's better to use pattern matching so any error that occurred can be handled appropriately.

I get the point, but consider this snippet that reads a directory and prints the accessed time for each entry:

use std::fs;
use std::path::Path;

fn main() {
    let path = Path::new(".");
    match fs::read_dir(&path) {
        Ok(entries) => {
            for entry in entries {
                match entry {
                    Ok(ent) => {
                        match ent.metadata() {
                            Ok(meta) => {
                                match meta.accessed() {
                                    Ok(time) => {
                                        println!("{:?}", time);
                                    },
                                    Err(_) => panic!("will be handled")
                                }
                            },
                            Err(_) => panic!("will be handled")
                        }
                    },
                    Err(_) => panic!("will be handled")
                }
            }
        },
        Err(_) => panic!("will be handled")
    }
}

I want to handle every possible error in the code above (the panic macro is just a placeholder). While the code above works, I think it's ugly. What is the idiomatic way to handle a case like this?

like image 812
Mas Bagol Avatar asked Oct 01 '16 11:10

Mas Bagol


People also ask

What is unwrap in Rust?

To “unwrap” something in Rust is to say, “Give me the result of the computation, and if there was an error, panic and stop the program.” It would be better if we showed the code for unwrapping because it is so simple, but to do that, we will first need to explore the Option and Result types.


2 Answers

I read that using unwrap on a Result is not a good practice in Rust.

It's not that easy. For example, read my answer here to learn a bit more. Now to your main problem:


Reduce right shift by passing Ok value to the outside

One big issue with your code is the right shift: for example, the meta.accessed() call is indented a whole lot. We can avoid this by passing the value we want to work with out of the match:

let entries = match fs::read_dir(&path) {
    Ok(entries) => entries, // "return" from match
    Err(_) => panic!("will be handled"),
};

for entry in entries {  // no indentation! :)
    // ...
}

That's already a very good way to make the code more readable.

Using the ? operator to pass the error to the calling function

Your function could return a Result<_, _> type in order to pass the error to the calling function (yes, even main() can return Result). In this case you can use the ? operator:

use std::{fs, io};

fn main() -> io::Result<()> {
    for entry in fs::read_dir(".")? {
        println!("{:?}", entry?.metadata()?.accessed()?);
    }
    Ok(())
}

Use helper methods of Result

There are also many helper methods, like map() or and_then(), for the Result type. and_then is helpful if you want to do something, if the result is Ok and this something will return a result of the same type. Here is your code with and_then() and manual handling of the error:

fn main() {
    let path = Path::new(".");
    let result = fs::read_dir(&path).and_then(|entries| {
        for entry in entries {
            let time = entry?.metadata()?.accessed()?;
            println!("{:?}", time);
        }
        Ok(())
    });

    if let Err(e) = result {
        panic!("will be handled");
    }
}

There really isn't only one way to do this kind of error handling. You have to get to know all the tools you can use and then need to choose the best for your situation. However, in most situations, the ? operator is the right tool.

like image 170
Lukas Kalbertodt Avatar answered Nov 12 '22 14:11

Lukas Kalbertodt


Result happens to have a lot of convenience methods for these kinds of things:

use std::fs;
use std::path::Path;

fn main() {
    let path = Path::new(".");
    match fs::read_dir(&path) {
        Ok(entries) => {
            for entry in entries {
                match entry.and_then(|e| e.metadata()).map(|m| m.accessed()) {
                    Ok(time) => {
                        println!("{:?}", time);
                    },
                    Err(_) => panic!("will be handled")
                }
            }
        },
        Err(_) => panic!("will be handled")
    }
}

And usually you will not have so much logic in main and will simply be able to use ? or try! in another function:

use std::fs;
use std::path::Path;

fn print_filetimes(path: &Path) -> Result<(), std::io::Error> {
    for entry in fs::read_dir(&path)? {
        let time = entry.and_then(|e| e.metadata()).map(|m| m.accessed())?;
        println!("{:?}", time);
    }

    Ok(())
}

fn main() {
    let path = Path::new(".");
    match print_filetimes(path) {
        Ok(()) => (),
        Err(_) => panic!("will be handled"),
    }
}
like image 23
mcarton Avatar answered Nov 12 '22 14:11

mcarton