Error Handling
It took a while for me to understand errors in Rust. However, one day, I came across the concept of recoverable and non-recoverable errors and that is when everything start making sense.
An unrecoverable
error occurs when it makes sense to terminate the code. One example would be failing to read a file because it is corrupt. If our only goal is to parse the file and print its contents, but we cannot even read it, then we'd classify this as a unrecoverable error.
A recoverable
error you can think of as when it is still safe or okay to proceed executing code. One example would be parsing lines from a file and one line has an unexpected structure. If we are okay with this, we can just skip this line and proceed to the next.
There are different ways of handling errors, some of which are listed below:
-
panic!
- Is a macro that, in single threaded applications, will cause the program to exit. -
unwrap
- Will panic if anOption<T>
isNone
or if aResult<T, Error>
isError
. -
expect
- Is similar tounwrap
but also displays a provided error message on panic. -
?
- Is used for error propagation and can be handled by e.g., upstream functions. This is a very elegant way of handling errors and is preferred overunwrap
andexpect
in real world appilcations.?
must always be inside a function that returns theResult
type.
Unrecoverable errors
In the code snippet below, we try to open a file that does not exist. Using .expect()
will cause a panic, but this is okay because we allow this to be an unrecoverable error.
use std::fs::File; fn main() { let _ = File::open("file_does_not_exist.txt").expect("Failed to open file."); }
Recoverable errors
In the following example, we implement a recoverable error for integer division using the ?
operator. The code looks quite complex for such a simple example, but the general pattern can be applied to other code as well.
-
We define a custom error type called
MathError
. We could define multipleMathError
types, but in our case,DivisionByZero
will suffice. -
We implement the
Display
trait for our custom error to avoid having to useDebug
print. -
We implement a function
divide
that returns aResult
, containing either af32
, or aMathError
. -
We implement a function
division
that uses the?
operator. Think of the?
asassume no error
, then we can returnOk(result)
. Ifresult
contains an error, the functiondivision
will make an early return. -
In
main
, we handle the division result accordingly.
#[derive(Debug)] enum MathError { DivisionByZero, } impl std::fmt::Display for MathError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { MathError::DivisionByZero => write!(f, "Cannot divide by zero!"), } } } fn divide(a: usize, b: usize) -> Result<f32, MathError> { match b { 0 => Err(MathError::DivisionByZero), _ => Ok(a as f32 / b as f32), } } fn division(a: usize, b: usize) -> Result<f32, MathError> { let result = divide(a, b)?; return Ok(result); } fn main() { let values: Vec<(usize, usize)> = vec![(1, 1), (1, 0)]; for (a, b) in values { match division(a, b) { Ok(r) => println!("{r}"), Err(e) => println!("{e}"), } } }
The takehome message here is that by handling recoverable errors, we avoid crashing our program when it does not need to.
Visit the official documentation for error handling to learn more. In addition, there are crates such as anyhow and thiserror that simplifies the generation of custom error types.