Error Handling
There are two types of error-handling in Rust:
- panic
Result
s
Ordinary errors are handled using Result
s.
Panic is the bad kind, it's for errors that should never happen.
Panic
Term: A program panics when it encounters something so messed up that there must be a bug in the program itself.
These are some things that can cause a panic:
- Out-of-bounds array access
- Integer division by zero
- Calling
.unwrap()
on anOption
that happens to beNone
- Assertion failure
Behavior: When a program panics, you can choose one of two ways that it'll be handled:
- Unwind the stack (this is the default)
- Abort the process
Unwinding
Process of a panic-triggered unwinding
- An error message is printed to the terminal.
- 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. This perpetuates upwards through the unwound stack.
- The thread exits. If the panicking thread was the main thread, then the whole process exits (with a nonzero exit code).
A panic is not a crash, nor undefined behavior. A panic's behavior is well-defined and safe.
Behavior: Panics occur per thread. One thread can be panicking while other threads are going on about their normal business.
It's possible to catch stack unwinding, which would allow the thread to survive and continue running using the standard library function std::panic::catch_unwind()
.
Aborting
Stack unwinding is the default panic behavior, but
Behavior: 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. The process will be aborted.- If you compile with a
-C panic=abort
flat, the first panic in the program immediately aborts the process. (this can be used to reduce compiled code size)
Result
Rust doesn't have exceptions.
Catching Errors
The most thorough way of dealing with errors via Result
is using a match
expression:
#![allow(unused)] fn main() { match get_weather(hometown) { Ok(result) => { display_weather(hometown, &report); } Error(err) => { println!("error querying the weather: {}", err); schedule_weather_retry(); } } }
match
es can be a bit verbose. But, Result
comes with a ton of useful methods for more concise handling.
Result Type Aliases
Sometimes you'll see Rust documentation that seems to omit the error type of a Result
. In such cases, a Result
type alias (a type alias is a shorthand for type names) is being used:
fn remove_file(path: &Path) -> Result<()>
Printing Errors
All error types implement a common trait: std::error::Error
.
Warning: Printing an error value does not also print out its cause. If you want to print all available information for an error, use the
print_error
function defined below.
#![allow(unused)] fn main() { use std::error::Error; use std::io::{Write, stderr}; /// Dump an error message to `stderr` /// If another error occurs in the process, ignore it fn print_error(mut err: &Error) { let _ = writeln!(stderr(), "error: {}", err); while let Some(cause) = err.cause() { let _ = writeln!(stderr(), "caused by: {}", cause); err = cause; } } }
Crate: The standard library's error types do not include a stack trace, but the
error-chain
crate makes it easy to define your own custom error type that supports grabbing a stack trace when it's created. It uses thebacktrace
crate to capture the stack.
Propagating Errors
Operator: You can add a
?
to any expression that produces aResult
. The behavior of?
depends on the state (Ok
orError
of theResult
):
- If
Ok
, it unwraps theResult
to get the success value inside- If
Error
, it immediately returns from the enclosing function, passing the error result up the call chain (see the rule below)
Rule: The
?
can only be used in functions whose return value is of typeResult
.
Working with Multiple Error Types
Some functions have the potential to return Error
s of a many different type (depending on the operation that triggered the error).
There are several approaches to dealing with multiple error types:
- Conversion: Define a custom error type (say,
CustomError
) and implement conversions fromio::Error
to the custom error type. - Box 'em up: The simpler approach is to use pointers. All error types can be converted to the type
Box<std::error::Error>
, which represents "any error", so we can define a set of generic type aliases all possible errors. This is the most idiomatic approach..
For generalizing all errors and results, define these type aliases:
#![allow(unused)] fn main() { type GenError = Box<std::error::Error>; type GenResult<T> = Result<T, GenError>; }
Tip: To convert any error to the
GenError
type, callGenError::from()
.
The downside of the GenError
approach is that the return type no longer communicates precisely what kinds of errors the caller can expect.
Tip: If you want to handle on particular kind of error, but let all other propagate out, use the generic method
error.downcast_ref::<ErrorType>()
. This is called error downcasting.
Dealing with Errors That "Can't Happen"
Operator: Instead of the
?
operator, which requires implementing error-handling, we can use the.unwrap()
method of aResult
to get theOk
value.
Warning: The difference between
?
and.unwrap()
is that if.unwrap()
is used onResult
that's in itsError
state, the process will panic. In other words, only use.unwrap()
when you're damn sureResult
isOk
.
Ignoring Errors
Idiom: If we really don't care about the contents of a
Result
, we can use the following idiomatic statement to silence warnings about unused results:let _ = writeln!(stderr(), "error: {}", err);
Handling Errors in main()
If you propagate an error long enough, eventually it'll reach the root main()
function, at which point, it can no longer be ignored.
Info: The
?
operator cannot be used inmain
becausemain
's return type is not aResult
. Instead, use.expect()
.
Behavior: Panicking in the main thread print an error message then exits with a nonzero exit code.