13 KiB
Error Handing
This is a factor of life in software, Rust has a number of features for handling errors. One feature is that Rust requires you to acknowledge the possibility of an error and take some action beofre our code will compile.
This requirement ensures that errors are handled before the possiblity could arise
This can be split into two major categories
- Recoverable - File a file not found, just need to report the problem to the user and retry the operation
- Unrecoverable - A symptom of bugs, like trying to access a location beyond the end of an array. Need to immediately stop the program
Many languages dont distinguish between the two kinds of errors and handle them the same way using mechanisms such as exceptions
Rust does not have exceptions
Instead it has the type Result< T, E>
for recoverable errors
It has the panc!
macro to stop eecution when an unrecoverable error occurs
Unrecoverable Errors
Whne bad things happen in your code and nothing you can do nothing about it then Rust has the panc!
macro
There are two ways to cause a panic:
- by taking an action that causes the code to paic (like accessing an array past the end)
- explicity calling
panic!
macro
By default these print a failure message, unwind, clean up the stack and then quit.
Using an environment variable you can also have Rust display the call stack when a panic occurs. This can make it easier to track down the source of the panic
When a call to panic!
occurs the error message will be contained in the last two lines. The first line will contain our message and the second is when te source of this panic occured
example
fn main() {
panic!("crash and burn");
}
This will output
thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This indicates that the panic occured in the file main.rs at the 2nd line on the 5th character
In this example it indicates that it is part of our source cdoe, looking there will show the panic!
macro
In other cases the panic!
call might be reported as someone else code where the panic!
macro was called
You can also use the traceback functions of the panic
call to figure ot the part of our code that caused the problem
To understand this an example will be used
fn main() {
let v = vec![1, 2, 3];
v[99];
}
Here we are tryin to access the 100th element, this is out of range and therefore Rust will initiate a error
In C, attempting to read beyond hte end of a data structure is undefined behavior, and you might get whatever is at the memory location, this would be something "random"
This is considered a buffer overread and can lead to security vulnerabilities, this would allow an attacker to be able to manipulate the index in such a way that they shouldnt be allowed to sore in that data structure.
Rust protects yo from this kind of vulnerability by casuing a panic if you try to read something out of range.
The note:
line tells us that we can set the RUST_BACKTRACE
environment variable to get a backtrace to show exactly what happened to casue the error.
The key to reading a backtrace is to start at the top and read until you see the files you wrote, that is where the problem originates.
The lines above that spot are code that our code has called, and the lines below are the code that called your code. These before-and-after lines might include core Rust code, std lib code or crates that you are using
You can set the backtrace by setting the RUST_BACKTRACE
environment variable to any value except 0
Example
RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5
1: core::panicking::panic_fmt
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14
2: core::panicking::panic_bounds_check
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:208:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:255:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:18:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/alloc/src/vec/mod.rs:2770:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
In order to a backtarace with this info, debug symbols must be enabled
Debug symbols are enabled by defualt when using cargo build
or cargo run
without the --release
flag
In line 6 of the backtrace points to the line in our project that causes the problem, that would be line 4 of src/main.rs
If we dont want our program to panc thne we sould start our investigation at the inidcated line we wrote
Unwinding the Stack or Aborting in Response to a Panic
unwinding in rust means that it walks back up the stack and cleans up the data form each function it encounters.
However walking back and cleaning up is a lot of work
Rust also allows yo to choose the alternative of immediately aborting, which means ends the program without cleaning up
Memory that the program was using will thne be clean up by the OS
If yo need your project's resultant binary as small as possible you can switch from unwinfing to aborting upon a panic
This can be done by adding panic = 'abort'
to the appropriate [profile]
section in your Cargo.toml
example of this
[profile.release]
panic = 'abort'
Recoverable Errors
You can use the enum Result
to handle most errors becasue they are not serious enough to warrant a panic
One example of this of a non serious error is opening a file and that operation fails becasue that file doesnt exist, you may want to create the file instead of terminating the process
the enum Result
is defined as
Which has two variants Ok
and Err
enum Result<T, E> {
Ok(T),
Err(E),
}
T
and E
are generic type parameters
T
reperesents the tpye of value that will be returned in a success case within th Ok
variant
E
reperesents the tpye of the error that will be returned in a failure case within the Err
variant
Because Result
has these generic type parameters we can use the Result
type and the functions defined on it in many different situations where the success value and error value we want to reutrn may differ
Lets use a function that retunrs a Result
value because the function could fail
use std::fs::File;
fn main () {
let greeting_file_result = File::open("hello.txt");
}
The return type of File::open
is a Result<T, E>
The generic parameter T
has been filled in by the implementation of File::open
with the type of the success value is a file handle (std::fs::File
)
The E
parameter is used in the error value which is std::io::Error
This return type indicates that call may succeed and reutnr a file handle that we can read and write to or it may fail if it doesnt exist, or not having the correct permissions
File::open
function needs a way to tell us whether it succeeded or failed hence the use of Result
enum which conveys this message of failure or success
When File::open
succeeds, the value in greeting_file_result
will be an instance of Ok
that contains a file handle
When it fails the value in greeting_file_result
will be an intance of Er
that contains more info about the kind of error that occurred
We need to add a match in order to use Result
here is one option
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
This is like the Option
enum
Result
enum and its variants have been brought into scope by the prelude, so you dont need to specifiy Result::
before the Ok
and Err
variants in the match arm
When the result is Ok
the code will reutrn the file
value out itself and then we can assign that file handle to the greeting_file
. After machh we can use the file for reading or writing.
The other arm in match
handles the Err
value we get from File::open
, in this case we choose to call the panic!
macro with more details about the error
Matching on Different Errors
You can take different actions depending on the type of failure
For example if a file doesnt open because it doesnt exist then maybe you want you want to create the file first instead of panicing and exiting the program
One way we can do this is by using a match
for the returned value Result<T, E>
Example
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
other_error => {
panic!("Problem opening the file: {other_error:?}");
}
},
};
}
The tpye that ``File::openreutrn inside thr
Errvarian is
io:Error`, this is a struct provided by the std library
This struct has a method kind
which can be called to get an io::ErrorKind
value
The enum io::ErrorKind
is also provided by a std library which has vairants representing the different kinds of errors that may result from an io
operation
The error we care about is Error::NotFound
, this indicates that the file that we try to open doesnt exist.
In this case we need to have a match on the outher thne use that on an inner match on error.kind()
In this inner match we create a file which can also fail hence in 2nd inner match to each return a file handle or panic out of the program
Alternatives to Using match with Result<T, E>
this match expression is very useful but also primitive and boilerplate
one example to condese this code is
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
This accomplishes the same thing as the the example above but it uses if and else and the unwrap_or_else
this is much cleaner to read even though it odesnt use and match statements
look at the exact definition unwrap_or_else
in the std library document
Shortcuts for Panic on Error: unwrap and expect
match
works well, but it can be a bit verbose and doesnt communicate clearly what the intent is
Result<T, E>
has many helper methods to do specific tasks
The unwrap
method that is a shortcut for the match
expression that was used before
If the Result
value is a Ok
variant, unwrap
will return the value inside the Ok
If the Result
is the Err
variant, unwrap
will call the panc!
macro for us
Here is an example of its use
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
If this code without a hello.txt it will panic with an error like this
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
You can also use the expect
method lets us also choose the panic!
error message
This is good for providing clearer error mesages and make tracking down the source of the panic easier
Here is an example of an expect
in use
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
expect
is used in the same way as unwrap
to retunr the file handle or call the panic!
macro
The message is used by expect
in its call to panic!
as the paramter that is passed to expect
rather than a defualt message
expect
is maninly used in production-quality code over unwrap
becasue it gives more context about why the opertaion is always expected to succeed
Propagating Errors
When a operation fails you may want to pass back the error instead of handling the error within the function itself