If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.
9.4.1 General Principles
Chapter 9.1, “Unrecoverable Errors and panic!”, already explained that Rust has two kinds of errors: recoverable and unrecoverable.
Calling panic! is equivalent to an unrecoverable error. Returning a Result type means the error is propagated, and such an error is recoverable.
If you think you can decide on behalf of the caller of your code that a situation is unrecoverable, then you can write panic!.
If your function returns Result, you are effectively giving the caller of the code the right to decide how to handle the error. The caller can then decide whether to recover from it, or it can consider the error unrecoverable and call panic! itself.
In short, if you are defining a function that may fail, prefer returning Result. If you believe a situation is definitely unrecoverable, use panic!.
9.4.2 Scenarios Where panic! Is Appropriate
When writing example code to demonstrate certain concepts, panic! is acceptable. In this kind of program, error handling often uses unwrap-style approaches that can trigger a panic. Here, unwrap acts like a placeholder, and code for different errors can later be written separately for each function.
You can use panic! when writing prototype code. At that stage, you may not yet know how to handle errors, and the unwrap and expect methods are very convenient during prototyping because they can trigger panics and leave clear markers in the code. Later, you can use those markers to handle the errors more specifically.
You can use panic! when writing test code. If a method call fails in test code, the entire test should be considered a failure, and failure is exactly what panic! can mark.
9.4.3 You Know Better Than the Compiler
Sometimes you can be certain that a function call will return Ok and will never panic. In that case, you can use unwrap. However, because the return type is something like Result, the compiler still thinks it may fail, while you know it cannot.
Take a look at an example:
use std::net::IpAddr;
fn main(){
let home: IpAddr = "127.0.0.1".parse().unwrap();
}
This example uses the IpAddr enum. In main, the string "127.0.0.1" is parsed. We know that "127.0.0.1" is a valid IP address, so the return value is definitely Ok, which means unwrap can be used here and will never panic.
9.4.5 Guiding Advice for Error Handling
When your code may end up in a bad state, it is usually best to use panic!. A bad state means that certain assumptions, guarantees, agreements, or invariants have been broken.
For example, invalid values, conflicting values, or missing values are passed into the code. And any of the following is true:
- This bad state is unexpected.
- Code after this point cannot continue to run if it is in this bad state.
- There is no good way to encode the information in the type being used.
Let’s look at some concrete scenarios:
- A meaningless parameter value is passed in:
panic! - External uncontrollable code returns an invalid state and you cannot fix it:
panic! - If failure is expected, such as parsing a string into a number:
Result - When your code operates on a value, you should first verify that the value is valid. If it is not:
panic!This is mainly for security reasons, because attempting to operate on an invalid value may expose vulnerabilities in the code. This is also why the standard library reports an error when code tries to access out of bounds: trying to access memory that does not belong to the current data structure is a common security problem. In addition, functions usually have certain contracts: they can run correctly only when the input satisfies specific conditions, and when those contracts are violated, they should panic. Breaking those contracts often indicates a bug on the caller’s side, and the resulting error should not be left for the caller to fix. It should be dealt with immediately by panicking.
9.4.6 Creating a Custom Type for Validation
Take the number guessing game from Chapter 2 as an example. Some code has been omitted:
fn main() {
loop {
// --snip--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
}
}
The original code has been changed a little:
- The type of
guesshas been changed fromu32toi32, so negative numbers can be accepted. - If the user enters a value less than 1 or greater than 100, the user is told that the secret number is between 1 and 100.
If parsing the string into an integer fails, continue is triggered to start the next iteration. If the number is outside the range 1 to 100, continue is triggered again. For this small program, the validation can be written directly inside main. In a large project, however, if every function needs validation, writing the validation logic over and over again inside each function would be quite troublesome.
In such cases, you can create a new type and put the validation logic into the constructor for that type. In this way, only values that pass validation can successfully create an instance, and you do not need to worry about whether the values you receive are valid later on.
Look at the example:
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
}
}
fn main() {
loop {
// --snip--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
let guess = Guess::new(guess);
match guess.value().cmp(&secret_number) {
// --snip--
}
}
new is the instance constructor. If the value is not between 1 and 100, it will panic!. If no panic occurs, a Guess instance is created and the value field is set to the value that was passed in.
There is also a method called value, which extracts the value of the value field from the struct and returns it.
In the main function below, you can remove the validation that checks whether the value is between 1 and 100 and instead use the Guess::new constructor to perform the validation.
If you need the actual value of guess, for example when using match, you can use the value method to get it.
Top comments (0)