mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-16 05:24:17 -06:00
320 lines
13 KiB
Markdown
320 lines
13 KiB
Markdown
# 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*](#recoverable-errors) - File a file not found, just need to report the problem to the user and retry the operation
|
|
- [*Unrecoverable*](#unrecoverable-errors) - 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
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
```toml
|
|
[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`
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
```rust
|
|
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::open` reutrn inside thr `Err` varian 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
|
|
|
|
```rust
|
|
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
|
|
```rust
|
|
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
|
|
```rust
|
|
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 |