Rust groups errors into two major categories: recoverable and unrecoverable errors.
Rust doesnβt have exceptions. Instead, it has the type Result<T, E>
for recoverable errors and the panic!
macro that stops execution when the program encounters an unrecoverable error.
Panic
By default, panic will print a failure message, unwind, clean up the stack, and quit. Unwind means that Rust will walk back up the stack and clean up the data from each function it encounters.
Unwind is a lot of work. Rust allows us to configure the program to abort, which ends the program without cleaning up - it will be handled by the OS. Add panic='abort'
to the appropriate [profile]
sections in your Cargo.toml file, e.g:
Result
Most errors arenβt serious enough to require the program to stop entirely.
Matching errors:
The unwrap
method is a shortcut method that will panic on error:
expect
works similarly but lets us choose the error message:
In production-quality code, most Rustaceans choose expect rather than unwrap and give more context about why the operation is expected to always succeed.
Error propagation
This pattern of propagating errors is so common in Rust that Rust provides the question mark operator ?
to make this easier.
There is a difference between what the match expression from Listing 9-6 does and what the ? operator does: error values that have the ? operator called on them go through the from function, defined in the From trait in the standard library, which is used to convert values from one type into another. When the ? operator calls the from function, the error type received is converted into the error type defined in the return type of the current function. This is useful when a function returns one error type to represent all the ways a function might fail, even if parts might fail for many different reasons.
For example, we could change the
read_username_from_file
function in Listing 9-7 to return a custom error type namedOurError
that we define. If we also defineimpl From<io::Error> for OurError
to construct an instance ofOurError
from anio::Error
, then the?
operator calls in the body ofread_username_from_file
will callfrom
and convert the error types without needing to add any more code to the function.
The ?
operator can only be used in functions whose return type is compatible with the value the ?
is used on (Result
, Option
, or another type that implements FromResidual
).
When to panic!
Panic when thereβs no way to recover and you donβt have to give the calling code the decision if itβs recoverable or not.
Returning Result
is a good default choice when youβre defining a function that might fail.
In situations such as examples, prototype code, and tests, itβs more appropriate to write code that panics instead of returning a Result
.
If you can ensure by manually inspecting the code that youβll never have an Err
variant, itβs perfectly acceptable to call unwrap
, and even better to document the reason you think youβll never have an Err
variant in the expect
text. Hereβs an example:
Itβs advisable to have your code panic when itβs possible that your code could end up in a bad state. In this context, a bad state is when some assumption, guarantee, contract, or invariant has been broken, such as when invalid values, contradictory values, or missing values are passed to your codeβplus one or more of the following:
- The bad state is something that is unexpected, as opposed to something that will likely happen occasionally, like a user entering data in the wrong format.
- Your code after this point needs to rely on not being in this bad state, rather than checking for the problem at every step.
- Thereβs not a good way to encode this information in the types you use.
If someone calls your code and passes in values that donβt make sense, itβs best to return an error if you can so the user of the library can decide what they want to do in that case. However, in cases where continuing could be insecure or harmful, the best choice might be to call panic!
and alert the person using your library to the bug in their code so they can fix it during development.
Similarly, panic!
is often appropriate if youβre calling external code that is out of your control and it returns an invalid state that you have no way of fixing.
However, when failure is expected, itβs more appropriate to return a Result
than to make a panic!
call. Examples include a parser being given malformed data or an HTTP request returning a status that indicates you have hit a rate limit. In these cases, returning a Result
indicates that failure is an expected possibility that the calling code must decide how to handle.
Panicking when the contract is violated makes sense because a contract violation always indicates a caller-side bug and itβs not a kind of error you want the calling code to have to explicitly handle. In fact, thereβs no reasonable way for calling code to recover; the calling programmers need to fix the code. Contracts for a function, especially when a violation will cause a panic, should be explained in the API documentation for the function.
However, having lots of error checks in all of your functions would be verbose and annoying. Fortunately, you can use Rustβs type system (and thus the type checking done by the compiler) to do many of the checks for you.
You can also create custom types for validation.