Rust Error Handling In Practice
Rust's error handling is marked by its capability to handle errors at compile-time without throwing exceptions. This article will delve…
Rust's error handling is marked by its capability to handle errors at compile-time without throwing exceptions. This article will delve into the basics of error handling in Rust, offering examples and best practices to guide you on your journey.
Types of Errors
In Rust, errors are classified into two primary types: recoverable and unrecoverable errors.
- Recoverable errors: These are errors that your program can recover from after it encounters them. For instance, if your program attempts to open a file that doesn't exist, it is a recoverable error because your program can then proceed to create the file. Rust represents recoverable errors with the
Result<T, E>
enum. - Unrecoverable errors: These are errors that the program cannot recover from, causing it to stop execution. Examples include memory corruption or accessing a location beyond an array's boundaries. Rust represents unrecoverable errors with the
panic!
macro.
Unrecoverable Errors with panic!
When your program encounters an unrecoverable error, the panic!
macro is used. This macro stops the program immediately, unwinding and cleaning up the stack. Here's a simple example:
fn main() {
panic!("crash and burn");
}
When this program runs, it will print the message "crash and burn", unwind, clean up the stack, and then quit.
Recoverable Errors with Result<T, E>
For recoverable errors, Rust uses the Result<T, E>
enum. This enum has two variants: Ok(value)
, which indicates that the operation was successful and contains the resulting value and Err(why)
an explanation of why the operation failed.
For instance, here's a function that attempts to divide two numbers, returning a Result
:
fn divide(numerator: f64, denominator: f64) -> Result<f64, &'static str> {
if denominator == 0.0 {
Err("Cannot divide by zero")
} else {
Ok(numerator / denominator)
}
}
When calling this function, you can use pattern matching to handle the Result
:
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(err) => println!("Error: {}", err),
}
The Question Mark Operator
Rust has a convenient shorthand for propagating errors: the ?
operator. If the value of the Result
is Ok
, the ?
operator unwraps the value and gives it. If the value is Err
, it returns from the function and gives the error.
Here's an example:
fn read_file(file_name: &str) -> Result<String, std::io::Error> {
let mut f = File::open(file_name)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
In this function, if File::open
or f.read_to_string
encounters an error, the function will immediately return that error. If not, the function will eventually replace the file's contents.
Best Practices
Here are some best practices for error handling in Rust:
- Use
Result<T, E>
for recoverable errors: If there's a chance your function might fail, it should return aResult
. This allows the calling code to handle the failure case explicitly. - Leverage the
?
operator: Remember to use the operator for error propagation if you're writing a function that returns aResult
. It makes your code cleaner and easier to read. - Make use of the
unwrap()
orexpect()
methods sparingly: These methods will cause your program to panic if they're called on aErr
variant. It's generally better to handle errors gracefully withmatch
the?
operator. - Don't panic!: Reserve
panic!
for situations when your code is in a state, it can't recover from. If there's any chance of recovery, return aResult
instead. - Customize error types: Rust allows you to define your own error types, which can give more meaningful error information. This can be particularly useful in more extensive programs and libraries.
- Handle all possible cases: When dealing with
Result
types, make sure your code handles both theOk
andErr
variants. Rust's exhaustive pattern matching will remind you of this, but it's a good principle.
Here's an example of creating a custom error:
use std::fmt;
#[derive(Debug)]
struct MyError {
details: String
}
impl MyError {
fn new(msg: &str) -> MyError {
MyError{details: msg.to_string()}
}
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for MyError {
fn description(&self) -> &str {
&self.details
}
}
In this example, we create a new error type MyError
that holds a String
. We implement fmt::Display
and Error
for MyError
so that it behaves like other errors.
Rust’s philosophy of “fearless concurrency” extends to its error-handling system. Rust enforces the handling of recoverable errors and allows the definition of custom error types, helping to produce more robust, reliable software.
More Advanced Error Patterns
As your programs become complex, so will the errors you encounter and need to handle. Rust provides several advanced patterns and tools to help with this.
Chaining Errors
It is often the case that an error in one function results from a series of other function calls that also return Result
. For this situation, Rust provides a map_err
function which can transform the error of a Result
using a function you provide.
Consider the following example:
fn cook_pasta() -> Result<Pasta, CookingError> {
let water = boil_water().map_err(|_| CookingError::BoilWaterError)?;
let pasta = add_pasta(&water).map_err(|_| CookingError::AddPastaError)?;
Ok(pasta)
}
Here, we're using map_err
to transform any error from boil_water
or add_pasta
into a CookingError
.
Using Error Traits
One of Rust's most powerful features is its trait system, which extends to its error handling. Specifically, Rust provides the std::error::Error
trait, which you can implement on your error types.
If you're writing a library, consider providing your own custom error type so that users of your library can handle errors from your library specifically.
Here's an example of creating a custom error type with the Error
trait:
use std::fmt;
use std::error::Error;
#[derive(Debug)]
pub struct CustomError {
message: String,
}
impl CustomError {
pub fn new(message: &str) -> CustomError {
CustomError {
message: message.to_string(),
}
}
}
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for CustomError {}
This CustomError
type implements the Error
trait, which means it can be used with the ?
operator and is interoperable with other kinds of errors.
Wrapping Errors
If you're dealing with many different kinds of errors in your function, you might want to use the anyhow
or thiserror
crate to make this easier.
The anyhow
crate provides the anyhow!
macro, which you can use to create an error of any type:
use anyhow::Result;
fn get_information() -> Result<()> {
let data = std::fs::read_to_string("my_file.txt")?;
// processing...
Ok(())
}
The thiserror
crate, on the other hand, is used for defining your own error types:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("failed to read file")]
ReadError,
#[error("unknown error occurred")]
Unknown,
}
Check out more articles about Rust in my Rust Programming Library!
Conclusion
Error handling is a crucial aspect of programming in Rust. While the language's approach to error handling might differ from what you're used to, it provides a robust and effective system for managing and dealing with errors in a way that leads to safer, more reliable code.
Remember the principles of using Result
for recoverable and panic!
unrecoverable errors, and take advantage of Rust's features for error handling like the ?
operator, error chaining, custom error types, and traits. With these tools and techniques, you'll be well-prepared to write error-free code in Rust.
Stay tuned, and happy coding!
Check out more articles about Rust in my Rust Programming Library!
Visit my Blog for more articles, news, and software engineering stuff!
Follow me on Medium, LinkedIn, and Twitter.
Check out my most recent book — Application Security: A Quick Reference to the Building Blocks of Secure Software.
All the best,
Luis Soares
CTO | Head of Engineering | Blockchain Engineer | Solidity | Rust | Smart Contracts | Web3 | Cyber Security
#blockchain #rust #programming #language #error #handling #smartcontracts #network #datastructures #data #smartcontracts #web3 #security #privacy #confidentiality #cryptography #softwareengineering #softwaredevelopment #coding #software