Chapter 7: Error Handling
Panic:
- Out-of-bounds array access
- Integer division by zero
- Calling
.expect()on aResultthat happens to beErr - Assertion failure
panic() accetps optional println() style arguments, for building an error message.
IMPORTANT: When a panic happens, by default:
- Stack is unwound: Any temoporary values, local variable, or arguments that the current function was using are dropped, in the reverse of the order they were created. Dropping a value simply means cleaning up after it: any
Strings orVecs the program wwas using are freed, any openFiles are closed, an so on. User-defineddropmethods are called too. ** In the particular case ofpirate_share, there's nothing to clean up.** Once the current function call is cleaned up, we move on to its caller, dropping is variables and arguments the same way. Then we move up to that function's caller, and so on up the stack. - Finally, the thread exits. It the panicking thread was the main thread, then the whole process exits (with a non-zero code).
Panic is safe. It doesn't violate any of Rust's safety rulse: even if you manage to panic in the middle of standard library method, it will ne ver leave a dangling pointer or a hald-initialized value in memory!
Panic is per thread. A parent thread can find out when a child thread panics and handle the error gracefully!
There is also a way to catch stack unwinding, allowing the thread to survive and continue running. The standard library function std::panic::catch_unwind() does this.
You can use threads and catch_unwind() to handle panic, making your program more robust. One imporatant caveat is that these tools only catch panics that unwind the stack. Not every panic proceeds this way!
If a drop() method triggers a second panic while Rust is still trying to clean up after the first,this is considered fatal. Rust stops unwinding and aborts the whole process.
Also, Rust's panic behavior is customizable. If you compile with -c panic=abort, the first panic in your program immediately aborts the process. (With this option, Rust does not need to know how to unwind the stack, so this can reduce the size of your compiled code).
How to handle a Result:
match is a bit verbose. So Result<T, E> offers a variety of methods:
result.is_ok(), result.is_err()result.ok()Returns the success value, if any, as anOption<T>result.err()Return the value, if any, as anOption<E>result.unwrap_or(fallback)Returns the success value, ifresultis a success result. Otherwise, it returnsfallback, discarding the error.result.unwrap_or_else(fallback_fn)This is the same, but instead of passing a fallback value directly, you pass a function or closure. Note: This is for cases where it would be wasteful to compute a fallback value if you're not going to use it. Thefallback_fnis called only if we have an error result.result.unwrap()Also returns the success value, ifresultis success. However, ifresultis an error result, this method panics.result.expect(message)This is the same asunwrap(), but provides a message that it prints in case of panic.result.as_ref()Converts aResult<T, E>toResult<&T, &E>.result.as_mut()Convers aResult<T, E>toResult<&mut T, &mut E>
Note: One reason these two methods are usefull is that all of the other methods listed here, except .is_ok() and .is_err(), consumes the result they operate on.
Result Type Aliases
Sometimes you'll see in Rust documentation that seems to omit the error type of Result:
#![allow(unused)] fn main() { fn remove_file(path: &Path) -> Result<()> }
This means that a Result type alias is being used. Modules often define a Result type alias to avoid having to repeat an error ttype that's used consistenly by almost every function in the module. For example the standard library's std::io module includes this line of code:
#![allow(unused)] fn main() { pub type Result<T> = result::Result<T, Error>; }
Printing an error value does not also print out its source. If you want to be suire to print all the available information:
#![allow(unused)] fn main() { use std::error:Error; use std::io::{Write, stderror}; fn print_error(mut err: &dyn Error) { let _ = writeln!(stderr(), "error: {}", err); while let Some(source) = err.source() { let _ = writeln!(stderr(), "caused by: {}", source); err = source; } } }
- Note that
writeln!macro works likeprintln!, except that it writes to a stream of your choice. - We could use the
eprintln!macro to do the same thing, buteprintln!panics if an error occurs.
Propagating Errors
It is simply too much code to use a 10-line match statement every place where something might go wrong.
#![allow(unused)] fn main() { let weather = get_weather(hometown)?; }
The behvaior of ? operator depends on whether this function returns a success result or an error result:
- On success, it unwraps the
Resultto get the success value inside. - On error, it immediately returns from the enclosing function, passing the error result up the call chain. To ensure that this works,
?can only be used on aResultin functions that have aResultreturn type.
? also works similarly with the Option type. In a function that returns Option, you can use ? to unwrap a value and return early in the case of None.
Working with Multiple Error ttype
#![allow(unused)] fn main() { use std::io::{Self, BufRead}; fn read_numers(file: &mut dyn BufRead) -> Result<Vec<i64>, io:Error> { let mut numbers = vec![]; for line_result in file.lines() { let line = line_result?; numbers.push(line.parse()?); } Ok(numbers) } }
Here the compiler will complain that it ? cannot convert a std::num::ParseIntError value to the type std::io:Error.
What should we do then?
- Type the
thiserrorcrate, which is designed to heop you define good error types with just a few lines of code. - A simpler approach is to use what's build into Rust. All of the standard library error types can be converted to the type
Box<dyn std::error::Error + Send + Sync + 'static>. This might seem mouthful. However,dyn std::error:Errorrepresents "any error" andSend + Sync + 'staticmakes it safe to pass between threads, which you'll often want.
type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;
- You could also consider using crate
anyhow
If you're calling a function that returns a GenericResult and you want to handle one particular kind of error but let all other propagate out, use the generic method error.downcast_ref::<ErrorType>(). It borrows a reference to the error, if it happens to be the particular type of error you're looking for.
#![allow(unused)] fn main() { loop { match compile_project(){ Ok(()) => return Ok(()), Err(err) => { if let Some(mse) = err.downcast_ref::<MissingSemicolonError>() { insert_semicolon_in_source_code(mse.file(), mse.line())?; continue; } return Err(err) } } } }