Request for help reviewing my understanding of error handling in Rust using Result, ?, Box<dyn Error>, and .map_err()

This tutorial helped me improve my understanding along with the Book and the docs. I would really appreciate it if someone could help me review my understanding of error handling using these techniques.

In a beginner project I've been working on--a tool to make grocery lists that I asked for feedback on recently in another post on this forum--I've tried to work on my understanding of using Result and the ? operator for error handling. Right now--things may well change--I'm using Box<dyn Error> in my different functions to pass down error messages to main(), like this ...
(this is like the CLI application example in The Rust Programming Language)

src/main.rs

use std::process;

fn main() {
    if let Err(e) = grusterylist::run() {
        eprintln!("Problem running application:\n{}", e);
        process::exit(1);
    }
}

... and using something like this code snippet to give more helpful errors to a user where necessary:

src/lib.rs
// ...
let groceries = serde_json::from_reader(reader).map_err(|err_msg| {
            format!(
                "Error deserializing groceries library!\n\
		 Something's wrong with the JSON file? \
		 See the example json files in the grusterylist repository.\n\
		 Here's the error message:\n\
		 '{}'",
                err_msg
            )
        })?;
// ...

In this case, the err_msg can be a pretty unclear message about a grammatical/structural issue in the JSON file the program is trying to read. So it's helpful to have something to make that clear.
This seems pretty verbose and a bit repetitive for now if you look at my code, so I guess my next step, following the tutorial I recommended, is to implement customized error types as structs.

I've worked on the error handling a bit and wanted to share in case it was useful or anyone had any comments. The code above will still provide useful context for what follows.

I had a first attempt at customized error handling:

// Customized handling of file reading errors
#[derive(Debug)]
pub enum ReadError {
    DeserializingError(serde_json::Error),
    PathError(Box<dyn Error>),
}

// Yup, you can't just return some string as an error message
impl fmt::Display for ReadError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ReadError::DeserializingError(e) => write!(
                f,
                "Error deserializing from JSON file:\n\
                 '{}'!\n\
		 Something's wrong with the JSON file?\n\
		 See the example json files in the \
		 grusterylist repository to see \
		 how things should look.\n",
                e
            ),
            ReadError::PathError(e) => write!(
                f,
                "Error: '{}'!\n\
		 Make sure file with that path \
		 can be accessed by the \
		 present working directory",
                e
            ),
        }
    }
}

// This is to make compatibility with the chain of Box<dyn Error> messaging
impl Error for ReadError {
    fn description(&self) -> &str {
        match *self {
            ReadError::DeserializingError(_) => "Error deserializing from JSON file!",
            ReadError::PathError(_) => "File does not exist!",
        }
    }
}

So, for example, in this case at least two things can go wrong--either the JSON file isn't there or there's something wrong with the way it's written as far as the program is concerned. In the latter case the error message that is automatically produced isn't very helpful. I use map_err to provide context to the Error my function will return:

let path = "groceries.json";

            let groceries = read_groceries(path).map_err(|e| {
                format!(
                    "Failed to read groceries file '{}':\n\
		     {}\n",
                    path, e
                )
            })?;

In the case of an error to do with the way the JSON file is written, the helper function for reading that JSON file uses map_err to return my customized error type, ReadError::DeserializingError.

pub fn read_groceries<P: AsRef<Path> + Copy>(path: P) -> Result<Groceries, Box<dyn Error>> {
        let reader = read(path)?;

        let groceries = serde_json::from_reader(reader)
            .map_err(ReadError::DeserializingError)?;

        Ok(groceries)
    }

So if that situation happens, the application exits and produces the following:

Problem running application: // from src/main.rs
Failed to read groceries file 'groceries.json': // from the function in src/lib.rs reading groceries.json 
Error deserializing from JSON file: // message from customized error
'key must be a string at line 1 column 2'! // automatically generated error message 
Something's wrong with the JSON file? // more customized error messaging ...
See the example json files in the grusterylist repository to see how things should look.

Your main() has a variable e. I don't know, but does that mean a destructor still needs to be run. to drop the e?
I do something like this instead so that in my main there are no
objects.

///  Run a sub main and call exit with the value returned.
///  Because there are no objects in this function, no destructors
///  will be called and it is safe to call std::process::exit()
///
///  From the [docs](https://doc.rust-lang.org/std/process/fn.exit.html)
///> If a clean shutdown is needed it is recommended to only call this function at a known point
///> where there are no more destructors left to run.
pub fn main() {
    std::process::exit(main2());
}

and have a main2, something like this..

fn main2() -> i32 {
    match grusterylist::run() {
        Ok(_) => 0,
        Err(error_code) => {
            eprintln!("Problem running application:\n{}", e);
            1
        }
    }
}
1 Like

Quoting from the docs for std::process::exit that you linked to, "If a clean shutdown is needed it is recommended to only call this function at a known point where there are no more destructors left to run."

I had to look up destructors and what the logic of what you were suggesting was about at first. According to the docs you're right, but I was under the impression I was using -- is boiler plate the right term here? -- code adapted but basically copied from The Rust Programming Language.

I wouldn't worry about this issue in your case—it would be pretty weird for an error type to have a destructor that does anything interesting. If you want to be really sure that e is destroyed before calling exit, you can do

fn main() {
    if let Err(e) = grusterylist::run() {
        eprintln!("Problem running application:\n{}", e);
+       drop(e);
        process::exit(1);
    }
}
1 Like

Thanks for clarifying this and sharing this solution. It's all good to know about.

I also noticed that in the docs for std::process:exit that @stonerfish shared it says, "a conventional way to use the function is to extract the actual computation to another function and compute the exit code from its return value":

fn run_app() -> Result<(), ()> {
    // Application logic here
    Ok(())
}

fn main() {
    std::process::exit(match run_app() {
        Ok(_) => 0,
        Err(err) => {
            eprintln!("error: {:?}", err);
            1
        }
    });
}
1 Like

Yeah, I guess wrapping it in main2 is kind of overkill.

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.