14
votes

I've been fumbling through Rust's documentation trying to execute a simple esoteric example for my own educational benefit more than practicality. While doing this, I can't seem to wrap my head around how Rust's error handling is meant to be used.

The programming example I'm using is to write a function that runs a command in a shell. From the result of the command I want to retrieve stdout (as a String or &str) and know whether or not the command failed.

The std::process::Command struct gives me the methods I want, but it seems that the only way to combine them is kludgy and awkward:

use std::process::Command;
use std::string::{String, FromUtf8Error};
use std::io::Error;


enum CmdError {
    UtfError(FromUtf8Error),
    IoError(Error),
}


// I would really like to use std::error::Error instead of CmdError,
// but the compiler complains about using a trait in this context.
fn run_cmd(cmd: &str) -> Result<String, CmdError> {
    let cmd_result = Command::new("sh").arg("-c").arg(cmd).output();

    match cmd_result {
        Err(e) => {
            return Err(CmdError::IoError(e));
        }
        Ok(v) => {
            let out_result = String::from_utf8(v.stdout);

            match out_result {
                Err(e) => {
                    return Err(CmdError::UtfError(e));
                }
                Ok(v) => {
                    return Ok(v);
                }
            }
        }
    }
}


fn main() {
    let r = run_cmd("echo 'Hello World!'");

    match r {
        Err(e) => {
            match e {
                CmdError::IoError(e) => {
                    panic!("Failed to run command {:}", e);
                }
                CmdError::UtfError(e) => {
                    panic!("Failed to run command {:}", e);
                }
            }
        }
        Ok(e) => {
            print!("{:}", e);
        }
    }
}

In particular, the nested match blocks inside run_cmd seem really awkward, and the nested match blocks in main are even worse.

What I'd really like to do is be able to use a more general class of error than FromUtf8Error or io::Error which I can type convert into easily from either concrete type, but it doesn't appear the type system is designed in this way, so I had to use the crude CmdError as somewhat of a union type instead.

I'm sure there's an easier way to do this which is more idiomatic, but I haven't found it from the documentation I've read so far.

Any pointers appreciated.

2

2 Answers

14
votes

Defining things like this is not a particularly neat thing at present; there are a few things you need to set up with your custom error type, but after you’ve done that things are a lot easier.

First of all, you will want to implement std::error::Error for CmdError (which requires std::fmt::Display and std::fmt::Debug), and then in order that try! can work automatically, std::convert::From<std::string::FromUtf8Error> and std::convert::From<std::io::Error>. Here are the implementations of those:

use std::error::Error;
use std::string::FromUtf8Error;
use std::fmt;
use std::io;

#[derive(Debug)]
enum CmdError {
    UtfError(FromUtf8Error),
    IoError(io::Error),
}

impl From<FromUtf8Error> for CmdError {
    fn from(err: FromUtf8Error) -> CmdError {
        CmdError::UtfError(err)
    }
}

impl From<io::Error> for CmdError {
    fn from(err: io::Error) -> CmdError {
        CmdError::IoError(err)
    }
}

impl Error for CmdError {
    fn description(&self) -> &str {
        match *self {
            CmdError::UtfError(ref err) => err.description(),
            CmdError::IoError(ref err) => err.description(),
        }
    }

    fn cause(&self) -> Option<&Error> {
        Some(match *self {
            CmdError::UtfError(ref err) => err as &Error,
            CmdError::IoError(ref err) => err as &Error,
        })
    }
}

impl fmt::Display for CmdError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            CmdError::UtfError(ref err) => fmt::Display::fmt(err, f),
            CmdError::IoError(ref err) => fmt::Display::fmt(err, f),
        }
    }
}

(The description method in the Error implementation could possibly return a string not based on the wrapped error, e.g. “failed to run command”. If one wants the details, they’ll still be there in Error.cause().)

After implementing that lot, things are a lot easier because we can use try!. run_cmd can be written thus:

fn run_cmd(cmd: &str) -> Result<String, CmdError> {
    let output = try!(Command::new("sh").arg("-c").arg(cmd).output());
    Ok(try!(String::from_utf8(output.stdout)))
}

Because try! uses the From infrastructure, this is all a lot simpler; the first line may return an Err(CmdError::IoError(_)) (for Command.output() returns Result<_, io::Error>), and the second line may return an Err(CmdError::UtfError(_)) (for String::from_utf8(…) returns Result<_, FromUtf8Error>).

Your main can also be somewhat simpler then, with the err branch not needing any further matching if you don’t care about the particular error; as it implements fmt::Display now, you can just use it directly.

Incidentally, in a format string, {:} should be written as {}; the : is superfluous if not followed by anything. ({:?} would work for showing Debug output, but you should prefer to use Display if it’s user-facing.)

2
votes

How to do it in 2019

Question mark operator (?)

Rust now has the question mark operator that makes propagating errors easy. You can read about it in the very complete chapter about recoverable errors in the Rust book.

Easy error type definition

There are several crates on crates.io that make it easy to define custom error types without having to write all the boilerplate that you had to write before. One very easy way to declare zero-overhead error types is to use the custom_error crate. I am the author of that crate.

Example: parsing the output of an external command

Combining the two points above, the example given in the question can be re-written in a very concise and readable way:

use std::string::FromUtf8Error;
use std::io;
use std::process::Command;
use custom_error::custom_error;

custom_error! {CmdError
    UtfError{source: FromUtf8Error} = "The command returned an invalid string: {}",
    IoError{source: io::Error} = "Unable to launch command: {}"
}

fn run_cmd(cmd: &str) -> Result<String, CmdError> {
    let out_bytes = Command::new("sh").arg("-c").arg(cmd).output()?.stdout;
    let out_string = String::from_utf8(out_bytes)?;
    Ok(out_string)
}

fn main() {
    match run_cmd("echo 'Hello World'") {
        Ok(res) => println!("{}", res),
        Err(e)  => eprintln!("{}", e)
    }
}