Error Handling in Rust
Contents
There are two different kinds of error handling in Rust: panic and Results
.
- Ordinary errors are handled using the
Result
type.Results
typically represent problems caused by things outside the program, like erroneous input, a network outage, or a permissions problem.
- Panic is for the kind of error that should never happen.
Panic
A program panics when it encounters something so messed up that there must be a bug in the program itself. Something like:
- Out-of-bounds array access
- Integer division by zero
- Calling
.expect()
on aResult
that happens to beErr
- Assertion failure
What these conditions have in common is that they are all the programmer’s fault.
When these errors that shouldn’t happen do happen, Rust can either unwind the stack when a panic happens or abort the process. Unwinding is the default.
Unwinding
In Rust, attempting to divide by zero triggers a panic, which typically proceeds as follows:
- An error message is printed to the terminal.
- If you set the
RUST_BACKTRACE
environment variable, Rust will also dump the stack at this point
- If you set the
- The stack is unwound.
- Any temporary values, local variables, 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
String
s orVec
s the program was using are freed, any openFile
s are closed, and so on. User-defineddrop
methods are called too. - Once the current function call is cleaned up, we move on to its caller, dropping its variables and arguments the same way. Then we move to that function’s caller, and so on up the stack.
- Any temporary values, local variables, 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
- The thread exits.
- If the panicking thread was the
main
thread, then the whole process exits (with a nonzero exit code).
- If the panicking thread was the
A panic is not a crash. It’s not undefined behavior. It’s more like a RuntimeException
in Java or a std::logic_error
in C++. The behavior is well-defined; it just shouldn’t be happening.
Panic is safe. It doesn’t violate any of Rust’s safety rules; even if you manage to panic in the middle of a standard library method, it will never leave a dangling pointer or a half-initialized value in memory. Panic is per thread. One thread can be panicking while other threads are going on about their normal business. The idea is that Rust catches the invalid array access, or whatever it is, before anything bad happens. It would be unsafe to proceed, so Rust unwinds the stack. But the rest of the process can continue running.
The standard library function std::panic::catch_unwind()
catches stack unwinding, allowing the thread to survive and continue running.
- This is the mechanism used by Rust’s test harness to recover when an assertion fails in a test.
- It can also be necessary when writing Rust code that can be called from C or C++, because unwinding across non-Rust code is undefined behavior.
You can use threads and catch_unwind()
to handle panic, making your program more robust. One important caveat is that these tools only catch panics that unwind the stack. Not every panic proceeds this way.
Aborting
Stack unwinding is the default panic behavior, but there are two circumstances in which Rust does not try to unwind the stack.
- 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. - 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.
Result
Rust doesn’t have exceptions. Instead, functions that can fail have a return type that says so:
fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>
- The
Result
type indicates possible failure. When we call theget_weather()
function, it will return either a success resultOk(weather)
, whereweather
is a newWeatherReport
value, or an error resultErr(error_value)
, whereerror_value
is anio::Error
explaining what went wrong. - Rust requires us to write some kind of error handling whenever we call this function. We can’t get at the
WeatherReport
without doing something to theResult
, and you’ll get a compiler warning if aResult
value isn’t used.
Catching Errors
The most thorough way of dealing with a Result
is using a match expression. This is Rust’s equivalent of try/catch
in other languages. It’s what you use when you want to handle errors head-on, not pass them on to your caller.
match get_weather(hometown) {
Ok(report) => {
display_weather(hometown, &report);
}
Err(err) => {
println!("error querying the weather: {}", err);
schedule_weather_retry();
}
}
match
is a bit verbose, so Result<T, E>
offers a variety of methods that are useful in particular common cases. Each of these methods has a match
expression in its implementation.
-
result.is_ok()
,result.is_err()
- Return a
bool
telling ifresult
is a success result or an error result.
- Return a
-
result.ok()
- Returns the success value, if any, as an
Option<T>
. Ifresult
is a success result, this returnsSome(success_value)
; otherwise, it returnsNone
, discarding the error value.
- Returns the success value, if any, as an
-
result.err()
- Returns the error value, if any, as an
Option<E>
.
- Returns the error value, if any, as an
-
result.unwrap_or(fallback)
-
Returns the success value, if
result
is a success result. Otherwise, it returnsfallback
value, discarding the error value. This is a nice alternative to.ok()
because the return type isT
, notOption<T>
. It works only when there’s an appropriate fallback value.// A fairly safe prediction for Southern California. const THE_USUAL: WeatherReport = WeatherReport::Sunny(72); // Get a real weather report, if possible. // If not, fall back on the usual. let report = get_weather(los_angeles).unwrap_or(THE_USUAL); display_weather(los_angeles, &report);
-
-
result.unwrap_or_else(fallback_fn)
-
This is the same, but instead of passing a fallback value directly, you pass a function or closure. This is for cases where it would be wasteful to compute a fallback value if you’re not going to use it. The
fallback_fn
is called only if we have an error result.let report = get_weather(hometown) .unwrap_or_else(|_err| vague_prediction(hometown));
-
-
result.unwrap()
- Returns the success value, if
result
is a success result. However, ifresult
is an error result, this method panics.
- Returns the success value, if
-
result.expect(message)
- This the same as
.unwrap()
, but lets you provide a message that it prints in case of panic.
- This the same as
-
result.as_ref()
- Converts a
Result<T, E>
to aResult<&T, &E
>.
- Converts a
-
result.as_mut()
- This is the same, but borrows a mutable reference. The return type is
Result<&mut T, &mut E>
.
- This is the same, but borrows a mutable reference. The return type is
All of the other methods listed here, except .is_ok()
and .is_err()
, consume the result they operate on. That is, they take the self
argument by value. Sometimes it’s quite handy to access data inside a result
without destroying it, and this is what .as_ref()
and .as_mut()
do for us.
- For example, suppose you’d like to call
result.ok()
, but you needresult
to be left intact. You can writeresult.as_ref().ok()
, which merely borrows result, returning anOption<&T>
rather than anOption<T>
.
Result Type Aliases
A type alias is a kind of shorthand for type names. Modules often define a Result
type alias to avoid having to repeat an error type that’s used consistently by almost every function in the module.
For example, the standard library’s std::io
module includes this line of code:
pub type Result<T> = result::Result<T, Error>;
- This defines a public type
std::io::Result<T>
. It’s an alias forResult<T, E>
, but hardcodesstd::io::Error
as the error type. In practical terms, this means that if you writeuse std::io;
, then Rust will understandio::Result<String>
as shorthand forResult<String, io::Error>
.
Printing Errors
Sometimes the only way to handle an error is by dumping it to the terminal and moving on.
The standard library defines several error types with names: std::io::Error
, std::fmt::Error
, std::str::Utf8Error
, and so on. All of them implement a common interface, the std::error::Error
trait, which means they share the following features and methods:
-
println!()
-
All error types are printable using this. Printing an error with the
{}
format specifier typically displays only a brief error message. Alternatively, you can print with the{:?}
format specifier, to get aDebug
view of the error.// result of `println!("error: {}", err);` error: failed to lookup address information: No address associated with hostname // result of `println!("error: {:?}", err);` error: Error { repr: Custom(Custom { kind: Other, error: StringError( "failed to lookup address information: No address associated with hostname") }) }
-
-
err.to_string()
- Returns an error message as a
String
.
- Returns an error message as a
-
err.source()
- Returns an
Option
of the underlying error, if any, that causederr
.- For example, a networking error might cause a banking transaction to fail, which could in turn cause your boat to be repossessed. If
err.to_string()
is"boat was repossessed"
, thenerr.source()
might return an error about the failed transaction. That error’s.to_string()
might be"failed to transfer $300 to United Yacht Supply"
, and its.source()
might be anio::Error
with details about the specific network outage that caused all the fuss. This third error is the root cause, so its.source()
method would returnNone
. - Since the standard library only includes rather low-level features, the source of errors returned from the standard library is usually
None
.
- For example, a networking error might cause a banking transaction to fail, which could in turn cause your boat to be repossessed. If
- Returns an
Printing an error value does not also print out its source. If you want to be sure to print all the available information, use this function:
use std::error::Error;
use std::io::{Write, stderr};
/// Dump an error message to `stderr`.
///
/// If another error happens while building the error message or
/// writing to `stderr`, it is ignored.
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;
}
}
- The
writeln!
macro works likeprintln!
, except that it writes the data to a stream.eprintln!
does the same thing, but it panics if an error occurs
The standard library’s error types do not include a stack trace, but the popular anyhow
crate provides a ready-made error type that does, when used with an unstable version of the Rust compiler. (As of Rust 1.50, the standard library’s functions for capturing backtraces were not yet stabilized.)
Propagating Errors
In most places where we try something that could fail, we don’t want to catch and handle the error immediately. It is simply too much code to use a 10-line match
statement every place where something could go wrong. Instead, if an error occurs, we usually want to let our caller deal with it. We want errors to propagate up the call stack.
Rust has a ?
operator that does this. You can add a ?
to any expression that produces a Result
, such as the result of a function call:
let weather = get_weather(hometown)?;
- On success, it unwraps the
Result
to get the success value inside. The type ofweather
here is notResult<WeatherReport, io::Error>
but simplyWeatherReport
. - 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 aResult
in functions that have aResult
return type.
There’s nothing magical about the ?
operator. You can express the same thing using a match
expression:
let weather = match get_weather(hometown) {
Ok(success_value) => success_value,
Err(err) => return Err(err)
};
- The only differences between this and the
?
operator are some fine points involving types and conversions.
In older code, you may see the try!()
macro, which was the usual way to propagate errors until the ?
operator was introduced in Rust 1.13:
let weather = try!(get_weather(hometown));
- The macro expands to a
match
expression, like the one earlier.
The ? operator sometimes shows up on almost every line of a function:
use std::fs;
use std::io;
use std::path::Path;
// pub type Result<T> = result::Result<T, Error>
fn move_all(src: &Path, dst: &Path) -> io::Result<()> {
for entry_result in src.read_dir()? { // opening dir could fail
let entry = entry_result?; // reading dir could fail
let dst_file = dst.join(entry.file_name());
fs::rename(entry.path(), dst_file)?; // renaming could fail
}
Ok(()) // phew!
}
?
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
:
let weather = get_weather(hometown).ok()?;
Working with Multiple Error Types
Reading numbers from a text file:
use std::io::{self, BufRead};
/// Read integers from a text file.
/// The file should have one number on each line.
fn read_numbers(file: &mut dyn BufRead) -> Result<Vec<i64>, io::Error> {
let mut numbers = vec![];
for line_result in file.lines() {
let line = line_result?; // reading lines can fail
numbers.push(line.parse()?); // parsing integers can fail
}
Ok(numbers)
}
// error[E0277]: `?` couldn't convert the error to `std::io::Error`
// |
// | fn read_numbers(file: &mut dyn BufRead) -> Result<Vec<i64>, io::Error> {
// | --------------------------- expected `std::io::Error` because of this
// ...
// | numbers.push(line.parse()?);
// | ^ the trait `From<ParseIntError>` is not implemented for `std::io::Error`
- Rust is complaining that the
?
operator can’t convert aParseIntError
value to the typestd::io::Error
. Reading a line from a file and parsing an integer produce two different potential error types. Rust tries to cope with theParseIntError
by converting it to aio::Error
, but there’s no such conversion
There are several ways of dealing with this. For example, the image
crate defines its own error type, ImageError
, and implements conversions from io::Error
and several other error types to ImageError
. If you’d like to go this route, try the thiserror
crate, which is designed to help you define good error types with just a few lines of code.
A simpler approach is to use what’s built into Rust. All of the standard library error types can be converted to the type Box<dyn std::error::Error + Send + Sync + 'static>
. dyn std::error::Error
represents “any error,” and Send + Sync + 'static
makes it safe to pass between threads. For convenience, you can define type aliases:
type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;
-
Change the return type of
read_numbers()
toGenericResult<Vec<i64>>
. With this change, the function compiles. The?
operator automatically converts either type of error into aGenericError
as needed. -
The downside of the
GenericError
approach is that the return type no longer communicates precisely what kinds of errors the caller can expect. The caller must be ready for anything.-
If you’re calling a function that returns a
GenericResult
and you want to handle one particular kind of error but let all others propagate out, use the generic methoderror.downcast_ref::<ErrorType>()
. It borrows a reference to the error, if it happens to be the particular type of error you’re looking for: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; // try again! } return Err(err); } } }
-
-
Also consider using the popular
anyhow
crate, which provides error and result types very much likeGenericError
andGenericResult
, but with some nice additional features.
The ?
operator does this automatic conversion using a standard method that you can use yourself. To convert any error to the GenericError
type, call GenericError::from()
:
let io_error = io::Error::new( // make our own io::Error
io::ErrorKind::Other, "timed out");
return Err(GenericError::from(io_error)); // manually convert to GenericError
Dealing with Errors That “Can’t Happen”
Suppose we’re writing code to parse a configuration file, and at one point we find that the next thing in the file is a string of digits. We want to convert this string of digits to an actual number. There’s a standard method that does this:
let num = digits.parse::<u64>();
The str.parse::<u64>()
method doesn’t return a u64
. It returns a Result
. It can fail, because some strings aren’t numeric.
We happen to know that in this case, digits
consists entirely of digits. If the code we’re writing already returns a GenericResult
, we can tack on a ?
and forget about it. Otherwise, we face the irritating prospect of having to write error-handling code for an error that can’t happen. The best choice then would be to use .unwrap()
, a Result
method that panics if the result is an Err
, but simply returns the success value of an Ok
:
let num = digits.parse::<u64>().unwrap();
"99999999999999999999".parse::<u64>() // overflow error
-
This is just like
?
except that if we’re wrong about this error, if it can happen, then in that case we would panic. But if the input contains a long enough string of digits, the number will be too big to fit in au64
. Using.unwrap()
in this particular case would therefore be a bug. Bogus input shouldn’t cause a panic. -
Situations do come up where a
Result
value truly can’t be an error. For example, theWrite
trait defines a common set of methods (.write()
and others) for text and binary output. All of those methods returnio::Results
, but if you happen to be writing to aVec<u8>
, they can’t fail. In such cases, it’s acceptable to use.unwrap()
or.expect(message)
to dispense with theResults
. -
These methods are also useful when an error would indicate a condition so severe or bizarre that panic is exactly how you want to handle it:
fn print_file_age(filename: &Path, last_modified: SystemTime) { let age = last_modified.elapsed().expect("system clock drift"); // ... }
- The
.elapsed()
method can fail only if the system time is earlier than when the file was created. This can happen if the file was created recently, and the system clock was adjusted backward while our program was running.
- The
Ignoring Errors
In the print_error()
function, we had to handle the unlikely situation where printing the error triggers another error. This could happen, for example, if stderr
is piped to another process, and that process is killed. The original error we were trying to report is probably more important to propagate, so we just want to ignore the troubles with stderr
, but the Rust compiler warns about unused Result
values:
fn print_error(mut err: &dyn Error) {
writeln!(stderr(), "error: {}", err); // warning: unused result
while let Some(source) = err.source() {
let _ = writeln!(stderr(), "caused by: {}", source);
err = source;
}
}
-
The idiom
let _ = ...
is used to silence this warning:let _ = writeln!(stderr(), "error: {}", err); // ok, ignore result
Handling Errors in main()
In most places where a Result
is produced, letting the error bubble up to the caller is the right behavior. But if you propagate an error long enough, eventually it reaches main()
, and something has to be done with it. Normally, main()
can’t use ?
because its return type is not Result
.
The simplest way to handle errors in main()
is to use .expect()
:
fn main() {
calculate_tides().expect("error"); // the buck stops here
}
- The error message is lost in the noise. Also,
RUST_BACKTRACE=1
is bad advice in this particular case.
Panicking in the main thread prints an error message and then exits with a nonzero exit code.
You can also change the type signature of main()
to return a Result
type, so you can use ?
:
fn main() -> Result<(), TideCalcError> {
let tides = calculate_tides()?;
print_tides(tides);
Ok(())
}
-
This works for any error type that can be printed with the
{:?}
formatter, which all standard error types, likestd::io::Error
, can be. -
This technique is easy to use and gives a somewhat nicer error message, but it’s not ideal. If you have more complex error types or want to include more details in your message, it pays to print the error message yourself:
fn main() { if let Err(err) = calculate_tides() { print_error(&err); std::process::exit(1); } }
Declaring a Custom Error Type
use std::fmt;
// json/src/error.rs
#[derive(Debug, Clone)]
pub struct JsonError {
pub message: String,
pub line: usize,
pub column: usize,
}
// Errors should be printable.
impl fmt::Display for JsonError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{} ({}:{})", self.message, self.line, self.column)
}
}
// Errors should implement the std::error::Error trait,
// but the default definitions for the Error methods are fine.
impl std::error::Error for JsonError { }
return Err(JsonError {
message: "expected ']' at end of array".to_string(),
line: current_line,
column: current_column
});
thiserror
crate:
use thiserror::Error;
#[derive(Error, Debug)]
#[error("{message:} ({line:}, {column})")]
pub struct JsonError {
message: String,
line: usize,
column: usize,
}
- The
#[derive(Error)]
directive tellsthiserror
to generate the code shown earlier.
Why Results?
The key points of the design of choosing Results
over exceptions:
- Rust requires the programmer to make some sort of decision, and record it in the code, at every point where an error could occur. This is good because otherwise it’s easy to get error handling wrong through neglect.
- The most common decision is to allow errors to propagate, and that’s written with a single character,
?
. Thus, error plumbing does not clutter up your code the way it does in C and Go. Yet it’s still visible: you can look at a chunk of code and see at a glance all places where errors are propagated. - Since the possibility of errors is part of every function’s return type, it’s clear which functions can fail and which can’t. If you change a function to be fallible, you’re changing its return type, so the compiler will make you update that function’s downstream users.
- Rust checks that
Result
values are used, so you can’t accidentally let an error pass silently (a common mistake in C). - Since
Result
is a data type like any other, it’s easy to store success and error results in the same collection. This makes it easy to model partial success. For example, if you’re writing a program that loads millions of records from a text file and you need a way to cope with the likely outcome that most will succeed, but some will fail, you can represent that situation in memory using a vector ofResult
s.
References