Skip to content

Instantly share code, notes, and snippets.

@williamyaoh
Last active April 2, 2018 19:28
Show Gist options
  • Save williamyaoh/be11d2f8c7c842d8fc1877ce6da16227 to your computer and use it in GitHub Desktop.
Save williamyaoh/be11d2f8c7c842d8fc1877ce6da16227 to your computer and use it in GitHub Desktop.
Example of how Rust gives static guarantees that you're handling errors correctly

Rust has a built-in generic type called Option<T>, so a value of type Option<i32> can be read as "I either have a valid 32-bit integer, or nothing." Using it as a return type tells your callers that you might fail, and makes sure that they handle that possibility correctly.

pub fn maybe_divide(x: i32, y: i32) -> Option<i32>

would be the signature of a function which might fail to produce a value (since y could be 0).

If Rust didn't have this type built-in, we could still define it ourselves:

pub enum Option<T> {
    None,
    Some(T)
}

It's this "enum-that-contains-values" pattern that lets us get compile-time guarantees that we're handling errors correctly. Because the only thing we can do, once we have some enum data, like Option<T>, is to pattern-match it:

pub fn maybe_divide(x: i32, y: i32) -> Option<i32> {
    if y == 0 {
        return None;
    } else {
        return Some(x / y);
    }
}

fn main() {
    let result = maybe_divide(10, 2);

    match result {
        None => println!("didn't get a value"),
        Some(n) => println!("got division result: {}", n)
    }
}

We can't accidentally use a bad value, because the variable n isn't even in scope on the None branch:

match result {
    Some(n) => println!("got division value: {}", n),
    None => println!("n: {}", n)  // Compile error! n is not in scope
}

We can't accidentally forget a case:

match result {
    Some(n) => println!("got division value: {}", n)
}
// Compile error! We failed to handle all possible cases

And we can't try to use the Option<i32> value directly:

let y = result + 5;  // Compile error! `result` and `5` are different types;
                     // `result` is an `Option<i32>` and `5` is an `i32`

So, representing our return values this way guarantees that we won't fail to handle exception cases, and that our program won't crash at runtime. If we call a function that returns an Option<T>, we have to handle that possibility of failure explicitly. If we change a function to start returning Option<T> instead of T, all our callers will see that change and have to handle it or face compile errors, since the return type has changed.

There are other ways to represent errors that build on the same idea of making sure errors are correctly handled. There's the type Result<T, E>, which can either be an Ok(T), when things succeed, or an Err(E), when things go wrong and you need to give more detail than just that it failed. Or, you can define your own enums if your particular situation is more complicated. Regardless of which one you choose, handling errors using this "enum-that-contains-values + pattern matching" strategy gives you confidence at compile time that errors are being handled correctly. It's how Rust does 99%[1] of its error handling, as do other languages like Haskell, OCaml, and Elm.


[1] The other 1% is through killing the current process; there isn't a structured exceptions system. Signalling errors this way is strongly discouraged, and people avoid it in production code. For example, I've told a bit of a fib; there is a way to get data out of an Option<T> without having to pattern match, called .unwrap(), which takes an option and returns the value inside it, or crashes the process if there isn't any data. But using it in serious code is considered a Very Bad Thing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment