Errors & Exceptions
Python uses exceptions to signal that something has gone wrong. A good Python app will anticipate what exceptions might be raised by various function or method calls and define the behavior when those exceptions occur.
Rust does not have exceptions. Instead it follows the “error as value” approach, where instead of raising an exception and breaking the normal control flow of the program, it will return an error instead of a value. The responsibility for handling both options falls to the calling code. Rust operations that can fail typically return a Result
type which can hold one of two things: the value or an error.
Part II included Rust code with expressions ending with .unwrap()
but I did not elaborate on why. Let’s get into it!
Result
is a type of Rust struct called an enum
, which organizes and labels related values. It is defined in std::result:
pub enum Result<T, E> {
Ok(T),
Err(E),
}
Angled brackets make another appearance here, which signifies that Result
is generic over types T
and E
. The enum holds two variants (Ok
, and Err
) which are named variants that wrap a value of a generic type.
Whenever you call a function that might fail, you should expect to receive a Result
. The Result
will either be an Ok
wrapping the good value, or an Err
wrapping the error value.
If you control the function and the input and know it will not fail, you might choose to skip the error checking and just access the value directly. Calling unwrap()
on an Ok
will extract the value from that wrapper. This is what I was doing in Part II.
Put this into Rust Playground:
pub fn get_69() -> Result<u8, String> {
Ok(69)
}
fn main() {
let known_good_value = get_69().unwrap();
println!("{:?}", known_good_value);
}
And run it to see:
69
Now let’s change the function to give an error instead:
pub fn get_69() -> Result<u8, String> {
Err(String::from("Can't get 69, not nice"))
}
Running this blows up with a panic message:
thread 'main' panicked at src/main.rs:6:37:
called `Result::unwrap()` on an `Err` value: "Can't get 69, not nice"
The unwrap
method quickly peels off the wrapper holding a successful value. However if you unwrap
an Err
, the runtime will panic, triggering an immediate cleanup and shutdown.
Python is like this, too. If a program raises an exception which is not caught, it will eventually reach the entry point of the program and the interpreter will clean up and shut down.
Matching
Dealing with Rust errors is similar to using Python’s structural pattern matching (see PEP-064), whereby you can match directly on the value inside the wrapping variant:
pub fn get_69() -> Result<u8, String> {
Ok(69)
// Err(String::from("Can't get 69, not nice"))
}
fn main() {
let unknown_value = get_69();
match unknown_value {
Ok(good_value) => println!("Got a value: {}", good_value),
Err(message) => println!("Got an error: {}", message),
}
}
Running this, we get:
Got a value: 69
And after changing the function to return the Err
instead:
Got an error: Can't get 69, not nice
The match
keyword allows us to set control flow depending on the wrapper variant, and to assign the wrapped value to a variable that we could use. This allows us to offload the “did the call succeed?” flow to the match block, and concentrate on the “then do this…” logic in each arm that follows the match.
The ? Operator
Sometimes an error is unrecoverable, so there’s no point handling it. For situations where defining a complete match
block would be overkill, we can use the ?
operator which will extract the wrapped values for either the Ok
or the Err
variant without using a full match
block.
Try it out:
pub fn get_69() -> Result<u8, String> {
// Ok(69)
Err(String::from("Can't get 69, not nice"))
}
fn main() {
let only_good_value = get_69()?;
}
This fails because the ?
operator can cause main
to return a String
instead of nothing, which is the default return type. We can adjust the function signature to specify the two possibilities expressed as a Result
:
pub fn get_69() -> Result<u8, String> {
// Ok(69)
Err(String::from("Can't get 69, not nice"))
}
fn main() -> Result<(), String> {
get_69()?;
Ok(())
}
The Ok
variant runs successfully with no output, and the Err
variant results in the message:
Error: "Can't get 69, not nice"
Notably, using the ?
operator on an Err
variant does not trigger a panic like unwrap
would — it just returns the wrapped error.
Type Conversions
The built-in Rust types are simple to convert. Like types can be converted in-line using cast
in an assignment expression:
fn main() {
let i: u64 = 420;
let j: u32 = i as u32;
println!(”{i}”);
println!(”{j}”);
}
Output:
420
420
However, please be careful! Casting between different bit length types can result in wrapping if the source value exceeds the range of the destination type:
fn main() {
let i: u64 = 420;
let j: i8 = i as i8;
println!(”{i}”);
println!(”{j}”);
}
Here, the i8
(8 bit signed integer) wraps from 127 to -128 when it reaches the upper limit of its range:
420
-92
Complex Conversions
As you move beyond the built-ins, you may want to perform more complex type conversions.