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.