This blog covers error handling in Rust with simple examples.
Rust does not have exceptions - it uses the Result
enum which is a part of the Rust standard library and it is used to represent either success (Ok)
or failure (Err)
.
enum Result<T,E> {
Ok(T),
Err(E),
}
When you are writing functions and need to handle potential problems, you can exit the program using the panic!
(macro) - this should be rare
panic
fn gen1() -> i32 {
let mut rng = rand::thread_rng();
let num = rng.gen_range(1, 10);
if num <= 5 {
num
} else {
panic!("{} is more than 5", num);
}
}
Here we call
panic!
if the random number is more than 5
To make our intentions clearer, we can return a Result
Using Result
fn gen2() -> Result<i32, String> {
let mut rng = rand::thread_rng();
let num = rng.gen_range(0, 10);
if num <= 5 {
Ok(num)
} else {
Err(String::from(num.to_string() + " is more than 5"))
}
}
In this case, if the number is less than equal to 5, we return the Ok
variant else, we return an Err
with a String
message
we could have used an
Option
return type in this case, but in this case, we are using result sincea number more than 5
is an erroneous scenario (in our fictitious use case)
Now we have a method which returns a Result
, let's explore how a caller would use this method. There are a few options:
- Use
panic!
- Acknowledge the problem and handle it
- Pass it on
panic
(again!)
This is the easiest but most often, the least desirable way to handle problems. Just panicking is a terrible idea. We can make it a little better, if not ideal
use expect
expect
is a shortcut - it is a method available on Result
which returns the content from the Ok
variant if its available. Otherwise, it panic
s with the passed in message and the Err
content
fn caller() -> String {
let n = gen2().expect("generate failed!");
n.to_string()
}
use unwrap
unwrap
is similar to expect
apart from the fact that it does not allow you to specify a custom message (to be displayed on panic!
)
fn caller() -> String {
let n = gen2().unwrap();
n.to_string()
}
We can do a little better with unwrap_or()
and use it to return a default value in case of an Err
fn caller() -> String {
let zero = gen2().unwrap_or(0);
zero.to_string()
}
In this case, we return
"0"
(String form) in case of an error
Handle the problem
fn caller() -> String {
let r = gen2();
match r {
Ok(i) => i.to_string(),
Err(e) => e,
}
}
In this case, we match
on the Result
: return a String
(using i.to_string()
) or return the Err
content (which also happens to be a String
in this case)
Not my problem.. pass it on
What if you did not want to handle the problem and need to propagate it to the caller (in a slightly different way)?
Instead of returning an explicit String
, you can return a Result<String,String>
where Ok
variant will contain a String
result and an Err
variant will (also) contain a String result explaining with error details
note that a
String
has been used to represent error (Err
) as well. This is just to keep things simple. Typically, you would use a special error type e.g.std::io:Error
etc.
Here is one way:
fn caller() -> Result<String, String> {
let r = gen2();
match r {
Ok(i) => Ok(i.to_string()),
Err(e) => Err(e),
}
}
We change the return type to Result<String, String>
, invoke the function and match
(which returns a Result<i32,String>
). All we need is to make sure we return the String
conversion of i32
. So we match against the returned Result
- if its Ok
, we return another Ok
which contains the String
version of i32 i.e. Ok(i) => Ok(i.to_string())
. In case of an error, we simply return it as is
?
operator
We can simplify this further by using the ?
operator. This is similar to the match
process wherein, it returns the value in the Ok
variant of the Result
or exits with by returning Err
itself. This is how its usage would look like in our case:
fn caller() -> Result<String, String> {
let n = gen2()?;
Ok(n.to_string())
}
The method is called - notice the ?
in the end. All it does it return the value in Ok
variant and we return its String
version using Ok(n.to_string())
. This takes care of the one half (the happy path with Ok) of the Result
- what about the other half (Err)? Like I mentioned before, with ?
its all taken care of behind the scenes, and the Err
with its value is automatically returned from the function!
Please note that the
?
operator can be used if you a returnsResult
orOption
or another type that implementsstd::ops::Try
What if the Err
type was different?
You can/will have a situation where you're required to return a different error type in the Result
. In such a case, you will need to implement std::convert::From
for your error type. e.g. this won't work since Result<String, NewErr>
is the return type
...
//new error type
struct NewErr {
ErrMsg: String,
}
...
fn caller5() -> Result<String, NewErr> {
let n = gen2()?;
Ok(n.to_string())
}
You need to tell Rust how to convert from String
to NewErr
in this case since the return type for the gen2()
function is Result<String,String>
. This can be done as such:
impl std::convert::From<String> for NewErr {
fn from(s: String) -> Self {
NewErr { ErrMsg: s }
}
}