Intro
This post corresponds to "Chapter 7. Error Handling" in the book. It's a short chapter on Rusts panic
and Result types.
Unlike Python, and similar to Go, Rust doesn't use Exceptions to handle errors. Instead, "expected" errors (things like EPERM or ENOENT, i.e. stuff that just can happen when running a program) are handled by returning an Result
with some Err
value.
"Unexpected" errors, on the other hand, are handled via panicking
Panic
Panics are a bit like RuntimeExceptions in Java; generally a panic is a bug in the program.
Panics are created by the Rust runtime or by the programmer with the panic!()
macro.
Conditions that will cause panics:
.expect()
on an Err resultAssertion failures
Out-of-bounds array access
Zero division
Panics are created per thread. Rust can either unwind the stack, or optionally just abort. By default no stack trace is printed unless the env has RUST_BACKTRACE=1
Unwinding the stack will drop any vars that were in use, open files are closed. User-defined drop methods are called as well.
It's possible to recover from a panic via the standard library function std::panic::catch_unwind()
. This is useful e.g. for test harnesses, or interfacing to ext. programs.
If during a .drop()
method a second panic happens, Rust will stop unwinding and abort the whole process. Aborting can also be made the default when compiling with -C panic=abort
– this can reduce the size of the binary.
Result
Functions that can fail should return a Result
; this can then be a success result Ok(v)
or an error Err(e)
The equivalent of try/except
for handling errors would be something like this match:
match get_weather(hometown) {
Ok(report) => {
, &report);
display_weather(hometown}
Err(err) => {
println!("error querying the weather: {}", err);
;
schedule_weather_retry()}
}
Methods for common cases:
result.is_ok(), result.is_err()
returns a boolresult.ok()
returns anOption<T>
either some value or Noneresult.unwrap_or(fallback)
returns a success or a default fallbackresult.unwrap_or_else(fallback_fn)
similar to above, but takes a funresult.unwrap(), result.expect(message)
will return a sucess value or panic;.expect()
lets you specify a messageresult.as_ref(), result.as_mut()
will convertResult<T, E>
to aResult<&T, &E>
(resp. a mutable ref). Useful if you don't want to consume the result right away.
Some libraries use a type alias, so that a shorthand can be used for declaring errors. E.g. std::io
has this
pub type Result<T> = result::Result<T, Error>;
...
fn remove_file(path: &Path) -> Result<()>
This hardcodes std::io::Error
as the error type, as this is being used throughout the lib
The various Error types implement a common interface which supports methods for printing and other inspection.
Printing with either the {}
or {:?}
format specifiers:
// result of `println!("error: {}", err);`
: failed to look up address information: No address associated with
error
hostname
// result of `println!("error: {:?}", err);` -- {:?} format prints extra debug info
: Error { repr: Custom(Custom { kind: Other, error: StringError(
error"failed to look up address information: No address associated with
hostname") }) }
To get the error message as a String use .to_string()
Those formats don't produce a stack trace, neither does err.to_string()
. There is a method to get at underlying errors, if any: err.source()
. There appears to be ongoing work on improving the stack trace situation in RFC 2504.
The anyhow crate has some helpers for error handling, and the thiserror crate has macros for defining custom errors.
Typically we let error results bubble up the call stack. We've already seen the ?
operator that conventiently unwraps success, and returns with an Err value:
let data = read_data_file(data_path)?;
// equiv. match expr.:
let data = match read_data_file(data_path) {
Ok(success_value) => success_value,
Err(err) => return Err(err)
};
Older Rust versions used the try!()
macro, as the ?
operator was only added in Rust 1.13. The ?
operator also works with Option
, where it'll early-return a None value.
The ?
of course implies that a function needs a Result return type. Often we will want to bubble up several types of errors, but how can we define several error types in a Result?
The first method is converting those errors to a custom error, and propagate that (see the thiserror crate for macros to support that). The second option is a conversion to Box
and define a generic error and result type like this:
type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;
// example usage ...
fn my_io_fn(file: &mut dyn BufRead) -> GenericResult<Vec<i64>> { ... }
There is no try / except X / except Y
in Rust. To handle different errors specifically, the err.downcast_ref()
method lets you fish out a ref to the actual error value:
match read_data() {
Ok(data) => data,
Err(err) => {
if let Some(eperm) = err.downcast_ref::<MissingSemicolonError>() {
.file(), mse.line())?;
insert_semicolon_in_source_code(msecontinue; // try again!
}
return Err(err);
}
}
For defining custom errors the thiserror crate – already mentioned above – has helpers. For example:
use thiserror::Error;
#[derive(Error, Debug)]
#[error("{message:} ({line:}, {column})")]
pub struct JsonError {
: String,
message: usize,
line: usize,
column}
// ...
// usage
return Err(JsonError {
: "foobared here".to_string(),
message: current_line,
line: current_column
column});
Coda
I've come to dislike Pythons way of error handling via exceptions because it creates an extra control path, and often for things that are not very exceptional at all to boot (looking at you, StopIteration and also many errors from the os lib, file perm. errors and such). Rusts error handling via Results for "expected" errors and the ?
operator – and panics for more serious things – feels much easier to reason about. It's a bummer we can't have stack traces in stable though, those things are useful. Second gripe, the procedure to create custom errors feels more complicated than it should be, even with the help of thiserror
.
The book makes a good point about partial results. Often when doing a bulk operation you're going to want to process the entire dataset, regardless if individual records fail to process or not, and only later deal with faults. By using Results, you can just naturally store those in a vector or some other collection, and do the error processing once all the data has been churned through.