Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a compact and idiomatic way to print an error and return without returning the error?

Tags:

rust

I'm writing a function that will be called in an infinite loop and only execute something when getting well-formed data from a web-service. If the service is down, returns non-json, or returns json we do not understand, the function should just log the error and return (to be called again after a pause).

I found myself copying and pasting something like this:

let v = match v {
    Ok(data) => data,
    Err(error) => {
        println!("Error decoding json: {:?}", error);
        return;
    }
};

The body of the error matcher would be different each time. Sometimes it's panic, sometimes it has different messages, and sometimes elements of error could be broken down further to form a better message, but the rest of the construct would be the same.

Is there a shorthand for this? I'm aware of the ? syntax, but that's for propagation. I don't feel that propagation will help with the scenario when you need slightly different processing in case of the error like in the scenario described above. This is because the particular differences in handling belong right here, not up the stack.

I have not written a lot of code in Rust yet so it is very likely that I'm missing something obvious.

In C#, the above would look something like this:

if (v == null)
{
  Console.WriteLine("Error decoding json!");
  return;
}

or

if (error != null)
{
  Console.WriteLine($"Error decoding json: {error}");
  return;
}

both of which is much less verbose than in Rust.

If I understood the comments below, one way of shortening would be something like this:

if let Err(error) = v {
    println!("Error decoding json: {:?}", error);
    return;
}
let v = v.unwrap();

This looks more compact, thank you. Is this idiomatic? Would you write it this way?

like image 681
Andrew Savinykh Avatar asked Jan 15 '18 09:01

Andrew Savinykh


1 Answers

I don't feel that propagation will help with the scenario when you need slightly different processing in case of the error like in the scenario described above. This is because the particular differences in handling belong right here, not up the stack.

This is something a custom error type can help with. In this case you have a common behavior ("log an error") and you want to do that in slightly different ways for different values. It makes sense to move the "log an error" part up to the caller (let's call the function try_poll):

loop {
    if let Err(e) = try_poll() {
        println!("{}", e);
    }
    sleep(100);
}

And create a type that implements Display, and From<E> for each error type E:

enum PollError {
    NetworkError(NetworkError),
    JsonParseError(JsonParseError),
}

impl fmt::Display for PollError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            PollError::NetworkError(ref e) => write!(f, "Error downloading file: {:?}", e),
            PollError::JsonParseError(ref e) => write!(f, "Error parsing JSON: {:?}", e),
        }
    }
}

impl From<NetworkError> for PollError {
    fn from(e: NetworkError) -> Self {
        PollError::NetworkError(e)
    }
}

impl From<JsonParseError> for PollError {
    fn from(e: JsonParseError) -> Self {
        PollError::JsonParseError(e)
    }
}

Now you can use ? to propagate the error, but the caller still doesn't have to be concerned with which error specifically it is.

fn try_poll() -> Result<(), PollError> {
    let data = try_fetch_content()?;
    let json = try_parse_json(data)?;
    println!("Parsed {:?}", json);
    Ok(())
}

(playground)


Ok, I want that, but without all the From implementations.

The tedious part about this is all the impl Froms, which are necessary because of the custom error type. If the only thing that will ever be done with an error is log and ignore it, a custom error type is not particularly useful -- the only thing that really needs to be returned is the error message itself.

In that case, have try_poll instead return Result<(), String>, and use Result::map_err to turn each individual error immediately into an error message, before using ? to propagate it:

fn try_poll() -> Result<(), String> {
    let data = try_fetch_content()
        .map_err(|e| format!("Error downloading file: {:?}", e))?;
    let json = try_parse_json(data)
        .map_err(|e| format!("Error parsing JSON: {:?}", e))?;
    println!("Parsed {:?}", json);
    Ok(())
}

(playground)

The first edition of The Rust Programming Language has this to say about String as an error type:

A rule of thumb is to define your own error type, but a String error type will do in a pinch, particularly if you're writing an application. If you're writing a library, defining your own error type should be strongly preferred so that you don't remove choices from the caller unnecessarily.

like image 166
trent Avatar answered Oct 05 '22 18:10

trent