Error Handling1
To run the program:
$ cargo run --bin error-handling
Compiling error-handling v0.1.0 ...
Rust groups errors into recoverable and unrecoverable errors:
- For a recoverable error, e.g. file not found error, we want to report it to the user and give the program (and the user) a chance to fix and retry the operation.
- Unrecoverable errors are always symptoms of bugs in the code, like trying to access a location beyond the end of an array, and so we want to immedaitely stop the program.
And so, Rust has two options:
Result<T, E>
for recoverable errors.panic!
for an unrecoverable error.
A panic is a "bad thing, and there's nothing you can do about".
By default, the program unwinds during a panic, and Rust walks back up the stack and cleans up data from each function it encounters. It's possible to change this behavior if your binary needs to be smaller/more efficient:
# Cargo.toml
[profile.release]
panic = 'abort'
Result is a simple two-variant (Ok
, Err
) enum:
enum Result<T, E> {
Ok(T),
Err(E),
}
For example, opening a file:
use std::fs::File;
fn main() {
match File::open("hello.txt") {
Ok(file) => { /* ... */ },
Err(error) => panic!("Problem opening the file: {:?}", error),
}
}
Or, another way:
let std:fs::File;
fn main() {
let file = File::open("hello.txt").unwrap_or_else(|error| {
/* ... */
});
}
It's easy to convert a Err
to a panic:
unwrap
:File::open("hello.txt").unwrap()
expect
:File::open("hello.txt").expect("Did not find expected hello.txt")
To propagate an error, use the ?
operator, an early return for Err
cases:
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
One more further change:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
-
In prototype code and tests, just
panic!
(or useunwrap
andexpect
). -
Where you have more knowledge than the compiler, also prefer panics:
let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid");
-
When a failure is expected, it's more appropriate to return a
Result
:- A parser being given malformed data
- An HTTP request returning a status code that indicates hitting a rate limit
-
When invalid values could put the user at risk, panic.
Another option is using Rust's type system to ensure we have valid values.
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}