The simplest guide to error handling in Rust
source link: https://kerkour.com/rust-error-handling/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Rust is loved for its reliability, and a good chunk of its reliability comes from its error handling ergonomics.
I know that there already are a few guides about error handling in Rust, but I found these guides to be too long and not straight to the point.
So here is the simplest and most straightforward guide to learn how to handle errors in Rust. The guide I would have loved to have if I started Rust today.
Overview
There are 2 types of errors in Rust:
- Non-recoverable errors (e.g., non-checked out of bounds array access)
- Recoverable errors (e.g., function failed)
Non-recoverable errors
For errors that can’t be handled and would bring your program into an unrecoverable state, we use the panic! macro.
fn encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
if key.len() != 32 {
panic!("encrypt: key length is invalid");
}
// ...
}
An alternative way to trigger a panic
is to use the assert! macro.
fn encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
assert!(key.len() == 32, "encrypt: key length is invalid");
// ...
}
That being said, handling errors in Rust is very ergonomic, so I see no good reason to ever intentionally panic
.
Recoverable errors
Errors that are meant to be handled are returned with the Result enum.
pub enum Result<T, E> {
Ok(T),
Err(E),
}
For example:
// Here, our error type is `String`
fn ultimate_answer(guess: i64) -> Result<(), String> {
if guess == 42 {
return Ok(());
}
return Err("Wrong answer".to_string());
}
Now, returning a String
as an error is not really useful. Indeed, the same function may return many different errors, so it becomes harder and harder to handle them with precision:
fn ultimate_answer(guess: i64) -> Result<(), String> {
if guess == 42 {
return Ok(());
} else if guess > 39 && guess <= 41 {
return Err("A little bit more".to_string());
} else if guess <= 45 && guess > 42 {
return Err("A little bit less".to_string());
}
return Err("Wrong answer".to_string());
}
Or, the same error can be returned by many different functions:
fn do_something() -> Result<(), String> {
// ...
return Err("Something went wrong".to_string());
}
fn do_something_else() -> Result<(), String> {
// ...
return Err("Something went wrong".to_string());
}
fn do_another_thing() -> Result<(), String> {
// ...
return Err("Something went wrong".to_string());
}
This is where we need to define our own Error
enum. Usually, we define 1 Error
enum by crate.
pub enum Error {
WrongAnswer,
More,
Less,
}
fn ultimate_answer(guess: i64) -> Result<(), Error> {
if guess == 42 {
return Ok(());
} else if guess > 39 && guess <= 41 {
return Err(Error::More);
} else if guess <= 45 && guess > 42 {
return Err(Error::Less);
}
return Err(Error::WrongAnswer);
}
Then, we may want to standardize the error message for each error case. For this, the community has settled on the thiserror crate.
#[derive(thiserror::Error)]
pub enum Error {
#[error("Wrong answer")]
WrongAnswer,
#[error("A little bit more")]
More,
#[error("A little bit less")]
Less,
}
Thanks to thiserror::Error
, your Error
enum now implements the std::error::Error trait and thus also the Debug and Display traits.
Then we can handle a potential error with match
.
fn question() -> Result<(), Error> {
let x = // ...
match ultimate_answer(x) {
Ok(_) => // do something
Err(Error::More) => // do something
Err(Error::Less) => // do something
Err(Error::WrongAnswer) => // do something
}
// ...
}
Or, the most common way to handle errors, forward them with ?
.
fn question() -> Result<(), Error> {
let x = // ...
ultimate_answer(x)?; // if `ultimate_answer` returns an error, `question` stops here and returns the error.
// ...
}
Which is a shortcut for:
fn question() -> Result<(), Error> {
let x = // ...
match ultimate_answer(x) {
Ok(_) => {},
Err(err) => return Err(err.into()),
};
// ...
}
Error conversion
Your program or library may use many dependencies, each with its own error types, but in order to be able to use ?
, your Error
type needs to implement the From trait for the error types of your dependencies.
#[derive(Error, Debug, Clone)]
pub enum Error {
#[error("Internal error.")]
Internal(String),
#[error("Not found.")]
NotFound,
#[error("Permission Denied.")]
PermissionDenied,
#[error("Invalid argument: {0}")]
InvalidArgument(String),
}
impl std::convert::From<std::num::ParseIntError> for Error {
fn from(err: std::num::ParseIntError) -> Self {
Error::InvalidArgument(err.to_string())
}
}
impl std::convert::From<sqlx::Error> for Error {
fn from(err: sqlx::Error) -> Self {
match err {
sqlx::Error::RowNotFound => Error::NotFound,
_ => Error::Internal(err.to_string()),
}
}
}
// ...
Unwrap and Expect
Finally, you can panic on recoverable errors with .unwrap() and .expect()
fn do_something() -> Result<(), Error> {
// ...
}
fn main() {
// panic if do_something returns Err(_)
do_something().unwrap();
}
// or
fn main() {
// panic if do_something returns Err(_) with the message below
do_something().expect("do_something returned an error");
}
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK