Error Handling
Rust uses explicit control flow for handling errors. That means that the errors are also values, hence they have types. This types are easily distinguishable on the return type:
fn read_from_file(path: PathBuff) -> Result<String, Error> {
todo!()
}
There are ways to skip this, through unsafe
programming, see the
following examples:
let value: Option<String> = None
let value: String = optional.unwrap()
According to the type we no longer have an optional, however since
the value of value
is None
it’ll panic
.
A panic
will be triggered if a fatal error happens at runtime,
for example:
fn main() {
let v = vec![10, 20, 30];
println!("v[100]: {}", v[100]);
}
Panics are non recoverable errors, they’re also unexpected. Rust encourages and makes a great effort into making errors as explicit as they can be.
Catch the Stack Unwinding
To answer this one we need to have two concepts crystal clear:
What does the Stack
mean?:
The Stack
refers as the Call Stack
. It is a data structure used
to manage function calls in a program. When a function is called,
all necessary information (local variables, return address) are pushed
onto the stack. When the function is called, its context is popped off
allowing the program to resume execution.
As an illustration let’s write a very simple program and see how it looks
on the Call Stack
:
fn f1() {
println!("F1");
}
fn f2() {
println!("F2");
}
f2()
What could cause the stack to unwind
and what does that mean?
When an exception occurs the program will start looking for an exception
handler further up the call stack. This process is called Stack Unwind
.
By default panic
will cause the stack to unwind. However, this can be caught
by panic::catch_unwind
:
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
"No problem here!"
});
println!("{result:?}");
let result = panic::catch_unwind(|| {
panic!("oh no!");
});
println!("{result:?}");
}
It won’t work if you have panic = 'abort'
option in your Cargo.toml
Error Propagation ?
This is called the try-operator ?
. It is used to return the errors
to the caller:
You can go from:
match some_expression {
Ok(value) => value,
Err(err) => return Err(err),
}
To:
some_expression?
This is heavily used for programming in the happy path
without needing to worry about error handling, only when
the appropriate time comes to do it.
There’s a caveat, the return type of the function that uses the try-operator
must be compatible with the nested function it calls. For instance, a function
returning a Result<T, Err>
can only apply the ?
operator on a function
returning a Result<AnyT, Err>
. It cannot apply the ?
operator on a function
returning an Option<AnyT>
or Result<T, OtherErr>
unless OtherErr
implements From<Err>
.