The Result type in rust is great! Combined with the ? operator, it makes error handling much more concise than other languages. Consider ? vs:

  • Go: if err != nil { return nil, err }
  • Java:  try { ... } catch { ... } & throws

When defining my own Result<T, E> type, leaving the T generic is really great, so you can have functions that return a variety of things, and have them return the same kind of error without having to specify it each time.

However, deciding what to use for the E was a litte more involved.  I laid out below what three approaches for what to use as E with pros and cons of each along with what they look like, and frequently downloaded crates that use that approach.

  • Option 1: Re-use an existing Result type
  • Option 2a: Wrapping - Use an enum for E with map_err
  • Option 2b: Wrapping - Use an enum for E with From impl's
  • Option 3: Boxing - Use Box<dyn error::Error>

I ended up going with option 2b -- an  enum with From impl's.

Option 1: Re-use an existing Result type

The first option is to not define a new Result type and just use an existing one.  For example, in a CLI, I found I was running a lot of functions that returned std::io::Result.   I could use that result type directly and bypass the E decision altogether.  (Another similar approach is to define a single error type of our own and return that everywhere).

use std::io;

fn get_string() -> io::Result<String> {
    let mut buffer = String::new();
    io::stdin().read_line(&mut buffer)?;
    Ok(buffer)
}

fn main() {
    get_string().unwrap();
}

(playground , Rust By Example)

Pros:

  • No E decision
  • Pretty common type

Cons:

  • Really falls apart when you branch out from io operations
  • what happens when I have an application specific error that's not related to io?
  • what about errors from other packages (like std::env)?

Example Users

Option 2a: Wrapping - Use an enum for E

The next option is to define my own Result type, and use an enum type for our E. (Fun fact: Result is actually just an enum of Ok(T), Err(E)!).  I can specify an enum variant for each of the existing error types I know I'll see, and I'm free to add new enum variants specific to my application.  I can use map_err to convert from the original error type to my enum.

use std::{io, env, result};
use std::fs::File;

#[derive(Debug)] <--- so we can unwrap()
enum CliError {
    Io(io::Error),
    EnvVarError(env::VarError)
}

type Result<T> = result::Result<T, CliError>;

fn get_string() -> Result<String> {
    File::open("foo.txt").map_err(|e| CliError::Io(e))?;
    std::env::var("PATH").map_err(|e| CliError::EnvVarError(e))?;

    Ok(String::from("Success!"))
}


fn main() {
    get_string().unwrap();
}

(playground)

Pros

  • Can handle any new error type
  • Is explicit about the allowable errors

Cons

  • Need to convert errors a bunch with
  • .map_err(|e| CliError::VarErr(e)
  • .map_err(|e| CliError::Io(e)
  • Need to add a new variant for each new error type

Example users:

Option 2b: Wrapping - Use an enum and implement the From trait on it

Those map_err calls can get a little annoying to write over and over.  Fortunately, there's an option to implement the From trait on my enum in order for the compiler to essentially do the map_err calls for me.  Here's what that looks like:

use std::{io, env, result};
use std::fs::File;

#[derive(Debug)] <--- so we can unwrap()
enum CliError {
    Io(io::Error),
    EnvVarError(env::VarError)
}


impl From<env::VarError> for CliError {
    fn from(error:env::VarError) -> Self {
        CliError::EnvVarError(error)
    }
}

impl From<std::io::Error> for CliError {
    fn from(error: std::io::Error) -> Self {
        CliError::Io(error)
    }
}

type Result<T> = result::Result<T, CliError>;

fn get_string() -> Result<String> {
    File::open("foo.txt")?;
    
    std::env::var("PATH")?;


    Ok(String::from("Success!"))
}


fn main() {
    get_string().unwrap();
}

(playground, Rust By Example)

Pros
Lets me use ?  without map_err most places

Cons
Needs a separate impl block for each new error type, which can be a little verbose

Example Users

Option 3: Boxing - Use Box<dyn error::Error>

A final option that needs way less code but is still flexible to error types is to use Box<dyn error::Error>.  This is an approach listed in the "rust-by-example" book as "Box-ing errors".  My initial reaction was: "wat".  I had seen a Box before (as a way to move something from the stack to the heap), but I hadn't seen dyn before.  It uses dynamic dispatch to call the functions associated with the std:error::Error trait on the heap allocated error object.

Here's what it looks like for our example:

use std::{result, error};
use std::fs::File;

type Result<T> = result::Result<T, Box<dyn error::Error>>;

fn get_string() -> Result<String> {
    File::open("foo.txt")?;
    
    std::env::var("PATH")?;


    Ok(String::from("Success!"))
}


fn main() {
    get_string().unwrap();
}

Pros

  • No more impl blocks or enum variants for each new error type, so it's much more concise
  • I don't even need to know what error types I'm encountering ahead of time (especially useful if I'm exposing generic functions to other libraries)

Cons

  • uses dynamic dispatch (slight performance cost)
  • puts things on the heap that could be in the stack (slight memory cost)
  • Needs calls to into in more places for your own error types (since they'll be in the stack, and need to be Boxed)

Example users:

Conclusion

In my case, I decided to go with Option 2b.  It had the right tradeoffs of being explicit so I could be more deliberate about new error scenarios as I introduced them (and I wasn't writing a library).  I also was dealing with a few error types very frequently (mosty io::Error), so adding a handful of impl blocks was a small one-time cost.

I hope this was helpful! If I've misstated anything or there are other options you think are worth considering, please do let me know.

Other interesting discussions

type Result<T> vs. type Result<T, E = My Error>

"In the docs, [...] type aliases are orangeish and enums are green" – Since Result is an enum, you can tell if someone is using the base type (green) or their own type alias (orange) when you search for Result.