mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-15 13:04:18 -06:00
699 lines
32 KiB
Markdown
699 lines
32 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 before our code will compile.
|
|
|
|
This requirement ensures that errors are handled before the possibility 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 don't 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 execution when an unrecoverable error occurs
|
|
|
|
## Unrecoverable Errors
|
|
When 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 panic (like accessing an array past the end)
|
|
- explicitly 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 the source of this panic occurred
|
|
|
|
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
|
|
|
|
This is used when you want the code that calls it to handle the error
|
|
|
|
here is an example is of this
|
|
```rust
|
|
use std::fs::File;
|
|
use std::io::{self, Read};
|
|
|
|
fn read_username_from_file() -> Result<String, io::Error> {
|
|
let username_file_result = File::open("hello.txt");
|
|
|
|
let mut username_file = match username_file_result {
|
|
Ok(file) => file,
|
|
Err(e) => return Err(e),
|
|
};
|
|
|
|
let mut username = String::new();
|
|
|
|
match username_file.read_to_string(&mut username) {
|
|
Ok(_) => Ok(username),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
```
|
|
|
|
This could be written in an even smaller way, but this is good for exploration
|
|
|
|
For example is has a return type of `Result<String, io::Error>` this is also a vaule of `Result<T, E>` where the values are filled in with `String` and `io:Error`, also note thta the ownership will transfer
|
|
|
|
If the function succeeds without any problems then the cod that calls it will receive an `Ok` value that holds a `String` - the `username` that this function read from the file
|
|
|
|
If this function encounters any problems then the code that calls this will recive an `Err` value that holds an instance of `io::Error` that contains more info aobut what the problems were.
|
|
|
|
`io:Error` as the return type was chosne becuase that happens to be the tpye of the error value returned from both operations we're callin in this function's body that might fail these functions that could fail are `File::open` and `read_to_string` method.
|
|
|
|
the last term does not need the `return` keyword do to it being both the last item and a expression
|
|
|
|
its up to the calling code how to hanlde the `Err` and `Ok` value, for example it could call a `panic!` if it gets a `Err` value
|
|
|
|
This pattern of propagting errors is so commin in Rust that Rust provides the question mark operator `?` to make it easier
|
|
|
|
#### A Shorcut for Popagation Errors: the ? Operator
|
|
|
|
Here is an implementation that is a bit smaller but deos the same thing as above
|
|
|
|
```rust
|
|
use std::fs::File;
|
|
use std::io::{self, Read};
|
|
|
|
fn read_username_from_file() -> Result<String, io::Error> {
|
|
let mut username_file = File::open("hello.txt")?;
|
|
let mut username = String::new();
|
|
username_file.read_to_string(&mut username)?;
|
|
Ok(username)
|
|
}
|
|
```
|
|
The `?` is placed adter a `Result` value is defined in almost the same way as the `match` expressions were dfeined in the previous example
|
|
|
|
If the `Result` value is an `Ok`, the value inside the `Ok` will get returned form this expression and it will continue
|
|
|
|
If the value is an `Err`, the `Err` will be returned from the whole function as if we had used the `return` keyword so that the error value gets propagated to te calling code
|
|
|
|
The difference between what `match` and what `?` does is that error values that have thr `?` operator called on them go through the `from` function, defined in thr `From` trait in the std library which is used to convert values fomr one type to another.
|
|
|
|
When `?` operator calls the `from` function, the error type is received is converted into the error type defined in the return type of the current function
|
|
|
|
This is useful when a function returns one error type to represent all the ways a functon might fail, even if parts might fail for many different reasons
|
|
|
|
In the context of the example, the `?` at the end of the `File::open` call will return the value inside an `Ok` to the variable `useranme_file`
|
|
|
|
If an error occurs the `?` operator will retunr early out of the whole function and give any `Err` value to the calling code.
|
|
|
|
This same thing applies to the `?` at the end of the `read_to_string` call
|
|
|
|
The `?` operartor eliminates a lot of boilerplate and make this function's implementation simpler
|
|
|
|
Here is an even shorter version of this code
|
|
```rust
|
|
use std::fs::File;
|
|
use std::io::{self, Read};
|
|
|
|
fn read_username_from_file() -> Result<String, io::Error> {
|
|
let mut username = String::new();
|
|
|
|
File::open("hello.txt")?.read_to_string(&mut username)?;
|
|
|
|
Ok(username)
|
|
}
|
|
```
|
|
|
|
Instead of creating a veriable `username_file` it has been chained to the call to `read_to_string` directly from the result of `File::open("hello.txt")?`
|
|
|
|
We still need a `?` at the end of `read_to_string` call, which still allows it to return an `Ok` value containg `username` when both `File::open` and `read_to_string` succeed rather than returning errors
|
|
|
|
The functionality is still the same as the two examples above
|
|
|
|
Here is an even shorter veriosn ising `fs::read_to_string`
|
|
```rust
|
|
use std::fs;
|
|
use std::io;
|
|
|
|
fn read_username_from_file() -> Result<String, io::Error> {
|
|
fs::read_to_string("hello.txt")
|
|
}
|
|
```
|
|
|
|
Reading a file into a string is so common that the std library provides the convenient `fs::read_to_string` function that opens a file, creates a new `String`, reads the contents of the file which puts the contents into that `String`, and returns it
|
|
|
|
But this doesnt show how to explain ho wto use it
|
|
|
|
### Where The ? Operator Can Be Used
|
|
The `?` operator can only be used in functions whose return type is compatible with the value in the `?` is used on
|
|
|
|
Because the `?` is defined to perform an early return of a value out of the function, in the same manner as the `match` expression.
|
|
|
|
In the match case there was an arm that retunred an `Err(e)` value that is compatible with its return
|
|
|
|
You can use `?` operator in main due to another crate being able to be used in a different crate
|
|
|
|
This requires that the retun type is compatable, `()` is not compatable with `Result`
|
|
|
|
For example this is not compatable
|
|
```rust
|
|
use std::fs::File;
|
|
|
|
fn main() {
|
|
let greeting_file = File::open("hello.txt")?;
|
|
}
|
|
```
|
|
|
|
But it will outbut something like this
|
|
```
|
|
$ cargo run
|
|
Compiling error-handling v0.1.0 (file:///projects/error-handling)
|
|
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
|
|
--> src/main.rs:4:48
|
|
|
|
|
3 | fn main() {
|
|
| --------- this function should return `Result` or `Option` to accept `?`
|
|
4 | let greeting_file = File::open("hello.txt")?;
|
|
| ^ cannot use the `?` operator in a function that returns `()`
|
|
|
|
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
|
|
help: consider adding return type
|
|
|
|
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
4 | let greeting_file = File::open("hello.txt")?;
|
|
5 +
|
|
6 + Ok(())
|
|
7 + }
|
|
|
|
|
|
|
For more information about this error, try `rustc --explain E0277`.
|
|
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
|
|
```
|
|
|
|
If you notice this states that you are only allowed to use `?` if it returns `Result`, `Option` or another type that implements `FromResidual`
|
|
|
|
To fix this error we have two options:
|
|
1. Change your return tpye of your function to be compatible with the value yo're using the `?` operator on, so long as there are no other restrictions preventing this
|
|
1. Use a `match` or one of the `Result<T, E>` methos to handle the `Result<T, E>` in the may most appropriate
|
|
|
|
The eror message also mentioned that `?` can be used with the `Option<T>` values as well
|
|
|
|
The behavior when `?` is called with an `Option<T>` is the same as `Result<T, E>`
|
|
|
|
If the value is `None`, this will be returned early as `None`
|
|
|
|
If the value is `Some`, the value inside the `Some` is the resultant value of the expression and the function continues
|
|
|
|
Here is an example where the function finds the last character of the first line in the given text
|
|
```rust
|
|
fn last_char_of_first_line(text: &str) -> Option<char> {
|
|
text.lines().next()?.chars().last()
|
|
}
|
|
```
|
|
|
|
This function returns `Option<char>` because its possible that there is a character there, but it is also possible that there is not
|
|
|
|
This code taskes a string slice and calls the `lines` method on it, which returns an iterator over the lines in the string. Becasue this function wants to examine the first line, it calls `next` on the iterator to get the first value from the iterator
|
|
|
|
If the string is emtpy then `next` will return `None`, if this is the case then yo can use `?` to stop and return `None` form `last_char_of_first_line`
|
|
|
|
If its not then `next` will return a `Some` value that contains a string slice of the first line in the string slice argument
|
|
|
|
The `?` extracts the string slice, then we can call `chars` on that string slice to get an iterator of its characters.
|
|
|
|
Since we want the last character in this first line, so we call `last` to return the last item in the iterator
|
|
|
|
This is an `Option` becasue it is possible that the first line is empty string
|
|
|
|
For example the string slice could contain `"\nhi"`
|
|
|
|
If there is a value then a `Some` will be returned and the `?` in this case would not be needed due to it being the last part of the function and it would do essentially nothing
|
|
|
|
Note that ou can use the `?` on a `Result` in a fuction that returns `Reuslt` and you can do the same on a function that returns a `Option`
|
|
|
|
You cannot mix and match
|
|
|
|
The `?` operator won't automatically convert a `Result` to an `Option` or vice versa
|
|
|
|
In those cases you can use methods like the `ok` method on `Result` or the `ok_or` method on `Option` to do the conversion explicitly
|
|
|
|
If you can implement this kind of logic then you would need to use other method calls or a `match` expression
|
|
|
|
Main can either return a `()` or one of the possibilities is to return a `Result<(), E>`
|
|
|
|
Here is a concerte example that return a `Result<(), Box<dyn Error>>` and added a return value `Ok(())` to the end
|
|
|
|
This ensures that the code will compile
|
|
```rust
|
|
use std::error::Error;
|
|
use std::fs::File;
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
let greeting_file = File::open("hello.txt")?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
The `Box<dyn Error>` type is a trait object, which for now means "any kind of error"
|
|
|
|
Using v`?` on a `Result` value in with the error type `Box<dyn Error>` is allowed becuase it allows any `Err` value to be returned early.
|
|
|
|
Even though the body of this `main` function will only ever return errors of the type `std::io::Eror` by specifying `Box<dyn Error>`, this will continue to be correct even if more code that returns other errors is added to the body of `main`
|
|
|
|
When a `main` returns a `Result<(), E>` the executable will exit with a value of `0` if `main` returns `Ok(())` and will return a nonzero value if `main` returns an `Err` value.
|
|
|
|
In C executalbes return a integer when they exit, programs that successfully return the `0` integer and reutrn an interger other than `0`
|
|
|
|
Rust also returns integers form executables to be compatible with this convention
|
|
|
|
`main` can return any types that implement the [`std::proccess::Termination`](https://doc.rust-lang.org/std/process/trait.Termination.html) trait which contains a function `report` that returns an `ExitCode`
|
|
|
|
See the std library documentation for more info on implementing the `Termination` trait for custom types developed by you
|
|
|
|
## To panic! or Not to panic!
|
|
When to call `panic!` and when to return `Result`
|
|
|
|
When the code panics there is no way to recover
|
|
|
|
you could call `panic!` for any erro sutuation whether there is a possible way to recover or not but then you make the decision that it is completely unrecoverable on behalf of the code
|
|
|
|
Or you could return a `Result` value and give the calling code option to handle it or attempt to recover in a way that's appropriate for its situation or you could decide that an speicfic `Err` value is unrecoverable and choose to call a `panic!` to turn a recoverable one into an unrecoverable
|
|
|
|
This makes returning a `Result` is a good default cohice for when yo are defining a function that may fail
|
|
|
|
In situations like prototype code, and tests its more appropriate to write code that panics instead of returning a `Result`
|
|
|
|
### Examples, Prototype Code and Tests
|
|
|
|
When writing an example to illustrate some concept, also inclduing robust error-handling code can make examples less clear
|
|
|
|
In examples its clear that a call to a mthod like `unwrap` that could panic is meant as a placeholder for the way you want your app to handle erros
|
|
|
|
Similarly the `unwrap` and `expect` methods are vary handy when prototyping, before you are ready to decide how to hanlde errors.
|
|
|
|
This leaves clear markers in your code for when you are ready to make your app more robust
|
|
|
|
If a method call fails in a test, you want the whole test to fail, even if that method isnt the functionality under the test
|
|
|
|
`panic!` is how a test is marked as a failure, calling `unwrap` or `expect` is exactly what should happen
|
|
|
|
### Cases in Which You Have More Info Than the Compiler
|
|
|
|
Its appropraite to call `unwrap` or `expect` whne you have some other logic that enusres the `Result` will have an `Ok` value, ut the logic is not something the compiler understands
|
|
|
|
You can sometimes still have a `Result` value even though the value inside should always be `Ok`
|
|
|
|
This is the case whenever you call a function or operation has a possibility of failing in generally, even though it is logically imposible in your situation.
|
|
|
|
This case happens when you can manually inspect the code and you would never expect an `Err` variant
|
|
|
|
It is unacceptable to call `unwrap` in these situations, it is better to document the reason you think you would never have an `Err` varaint in the `expect` text
|
|
|
|
Here is an example
|
|
```rust
|
|
use std::net::IpAddr;
|
|
|
|
let home: IpAddr = "127.0.0.1"
|
|
.parse()
|
|
.expect("Hardcoded IP address should be valid");
|
|
```
|
|
|
|
In this example it is creating an `IpAddr` instance by parsing a hard coded string
|
|
|
|
We can see that the string is a valid IP address, so it's acceptable to use `expect` here
|
|
|
|
Having a hardcoded, valid string doesnt change the return type of the `parse` method, we still get a `Result` value
|
|
|
|
The compiler will force us to handle the `Result` as if the `Err` value is a possiblity
|
|
|
|
This is because the compiler cannot see the logic from a hard coded string which doesnt have the possibility of failure
|
|
|
|
Mentioning the assumption that this IP address is hardcoded will prompt us to change `expect` to better error-handing code if in the future, we need to get the IP address from some other source instead
|
|
|
|
## Guidelines for Error Handling
|
|
Its advisable to have your code panic when its possible to get ino a bad state
|
|
|
|
A *bad state* is when some assumption, guarantee, contract, or invariant has been broken such as when invalid values, contradictory values, or missing values are passed to your code plus one or more of the following:
|
|
- The bad state is somthing that is unexpected which is not something that is a likely problem (a user entering data in the wrong format)
|
|
- The code needs to rely on not being in a bad state, rather than checking at every step
|
|
- There is not a good way to encode this info in the types you use
|
|
If someoneelse calls your code and passes in bad values or values that dont make sense, its best to return an error if you can, so that the lbirary's user can decide what they want to do in that case
|
|
|
|
In some cases where continuing could be insecure or harmful, the best choice might bbe to call a `panic!` and alert the library user to the bug in thier code so that they can ifx it during development
|
|
|
|
`panic!` is so often appropriate if you are calling external cod that is out of your control and it retunrs an invaild state that you have no way of fixing
|
|
|
|
If a failure is expected then it is more appropriate to return a `Result` than to make a `panic` call
|
|
|
|
Examples of this include a parser being being malformed data or an HTTP requst retunring a status that indicates ou have hit a rate limit.
|
|
|
|
In these caes it would be more appropriate to return a `Result` indicating that a failure is an expected possiblity that the calling code must decide how to handle.
|
|
|
|
When your code performs an opertation that could put a user at risk if is called using invalid values, your code should verify the values first then paic if the values aren't valid
|
|
|
|
This is mostly for safety reasons, attempting to operate on invalid data that could expose your code to vulnerabilities
|
|
|
|
This would be the main reason the std library will call `panic!` if you attempt an out-of-bounds memory access
|
|
|
|
Trying to access memory that doesnt belong to the current data struct, this is a common security problem.
|
|
|
|
Functions often have *contract*, their behavior is only guaranteed if the inputs meet particlar requirements.
|
|
|
|
Panicking when the cotnract is violated makes sense because a contract violation always indicates a caller-side bug and its not a kind of error you want the calling code to have to handle explicitly.
|
|
|
|
In this case there is no reasonable way for the calling code to reacover or fix the code becasue the programmer needs to dix the code.
|
|
|
|
Contracts for a fnction, especially when a violation will cause a panic, this should be explained in the API docs for the function.
|
|
|
|
Having lots of error chcks in all of your functions would be verbos and annoying.
|
|
|
|
Instead you can use Rust's type system and thus the compiler to do many of the checks for you.
|
|
|
|
If your function has a particular tpe as a parameter, you can proceed with your code's logic knowing that the compiler will ensure that you have a vaild value
|
|
|
|
For example, using u32 instead of i32 enusres you will never have a negative number, or you cant pass a `Option` into a function with a conrete tpye, the code won't even compile becuase it cant take in an `Option`
|
|
|
|
## Creating Custom Types for Validation
|
|
This is useful for enchancing or guiding the user toward valid data, and have different behavior when a user give an almost invalid data
|
|
|
|
Having too many checks can impact performance if you had many many functions with a requirement that is the same.
|
|
|
|
Instead you can make a new type and put the validations in a function to create an instance of the tpye rather than needing to repeat the validations everywhere
|
|
|
|
This makes it more safe for functions to use the new type in their signatures and confidently use the values they recieve.
|
|
|
|
Here is a way to implement this kind of type for the original guessing game from ch2
|
|
```rust
|
|
pub struct Guess {
|
|
value: i32,
|
|
}
|
|
|
|
impl Guess {
|
|
pub fn new(value: i32) -> Guess {
|
|
if value < 1 || value > 100 {
|
|
panic!("Guess value must be between 1 and 100, got {value}.");
|
|
}
|
|
|
|
Guess { value }
|
|
}
|
|
|
|
pub fn value(&self) -> i32 {
|
|
self.value
|
|
}
|
|
}
|
|
```
|
|
|
|
The guess struct can only hold a i32 value that is private
|
|
|
|
In its associated functions there is a `new` function or a contructor that first checks if the value is between 1-100 and reutrns a new instance of the function if it is valid otherwise it calls a `panic!`
|
|
|
|
It can take in an `i32` but it still calls a `panic!` if it is out of range, the panic is ued to alert the progammer who is writing thr calling ocde that they have a bug to fix becuae calling a value outside of the range would violate the contact that `Guess::new` depends on.
|
|
|
|
The conditions in which `Guess:new` might panic should be told in its public-facing API docs.
|
|
|
|
A function that has a parameter or returns only numbers between 1 and 100 could declarre in its signature that it takes in or returns a `Guess` rather than an `i32` and you wouldnt need to addtionaly checks in its body |