Error Handling
Master different error handling techniques in Rust, including `Result` type and `panic!` macro.
Custom Error Types in Rust
Rust's error handling relies heavily on the Result<T, E>
type. While using standard error types like Box<dyn Error>
can be convenient, creating custom error types provides more context and makes your code more robust and maintainable. This document explains how to define custom error types using enum
or struct
and how to implement the Error
trait.
Why Custom Error Types?
- Clarity: Custom error types clearly communicate the specific failures that can occur in your code.
- Context: They can carry additional information about the error, aiding in debugging and recovery.
- Maintainability: Makes the code easier to understand and refactor in the long run.
- Pattern Matching: Enable precise error handling using pattern matching, allowing you to respond differently to distinct error conditions.
Defining Custom Error Types with enum
Using an enum
is often the preferred way to define custom error types when you have a set of distinct error conditions that can occur. Each variant of the enum
represents a different type of error.
use std::fmt;
use std::error::Error;
#[derive(Debug)]
enum MathError {
DivisionByZero,
NegativeSquareRoot,
Overflow,
}
impl fmt::Display for MathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MathError::DivisionByZero => write!(f, "Division by zero"),
MathError::NegativeSquareRoot => write!(f, "Square root of a negative number"),
MathError::Overflow => write!(f, "Arithmetic overflow"),
}
}
}
impl Error for MathError {}
fn safe_divide(x: i32, y: i32) -> Result<i32, MathError> {
if y == 0 {
Err(MathError::DivisionByZero)
} else {
Ok(x / y)
}
}
fn main() {
match safe_divide(10, 0) {
Ok(result) => println!("Result: {}", result),
Err(error) => println!("Error: {}", error),
}
}
Explanation:
- We define an
enum
calledMathError
with three variants:DivisionByZero
,NegativeSquareRoot
, andOverflow
. - We implement the
fmt::Display
trait to provide a human-readable description of each error variant. This is what is printed when you useprintln!("{}", error)
. - We implement the
Error
trait for our custom error type. This is essential for using it with theResult
type and for interoperability with other error handling mechanisms in Rust. The default implementation is often sufficient, requiring no explicit code within the `impl` block (i.e., `impl Error for MathError {}` is enough). - The
safe_divide
function returns aResult
that can either be a successfuli32
value or aMathError
.
Defining Custom Error Types with struct
A struct
is suitable when you need to associate specific data with an error. This allows you to store more detailed information about what went wrong.
use std::fmt;
use std::error::Error;
#[derive(Debug)]
struct FileParseError {
filename: String,
line_number: usize,
message: String,
}
impl fmt::Display for FileParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Error parsing file {}:{} - {}", self.filename, self.line_number, self.message)
}
}
impl Error for FileParseError {}
fn parse_file(filename: &str) -> Result<(), FileParseError> {
// Simulate a parsing error
Err(FileParseError {
filename: filename.to_string(),
line_number: 42,
message: "Invalid syntax".to_string(),
})
}
fn main() {
match parse_file("data.txt") {
Ok(_) => println!("File parsed successfully!"),
Err(error) => println!("Error: {}", error),
}
}
Explanation:
- We define a
struct
calledFileParseError
that contains fields for the filename, line number, and a descriptive message. - The
fmt::Display
implementation formats the error message to include all the relevant details. - Again, we implement the
Error
trait. - The
parse_file
function simulates a parsing error and returns aFileParseError
with the appropriate data.
Implementing the Error
Trait
Implementing the Error
trait is crucial for custom error types. It signifies that your type represents an error and allows it to be used with standard error handling mechanisms. The minimum requirement is to implement the trait itself: impl Error for MyErrorType {}
. Optionally, you can override the source()
method to indicate the underlying cause of the error (if applicable). The source()
method returns an Option<&dyn Error>
. If the current error was caused by another error, it should return Some(&underlying_error)
. If there is no underlying error, it should return None
.
use std::fmt;
use std::error::Error;
#[derive(Debug)]
struct UnderlyingError;
impl fmt::Display for UnderlyingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "An underlying error occurred")
}
}
impl Error for UnderlyingError {}
#[derive(Debug)]
struct MyError {
message: String,
source: Option<UnderlyingError>, // Use Option for optional source error.
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MyError: {}", self.message)
}
}
impl Error for MyError {
fn source(&self) -> Option<&dyn Error> {
self.source.as_ref().map(|e| e as &dyn Error) //Needed to avoid type mismatch
}
}
fn main() {
let err = MyError { message: "Something went wrong".to_string(), source: Some(UnderlyingError)};
println!("Error: {}", err);
if let Some(source) = err.source() {
println!("Caused by: {}", source);
} else {
println!("No underlying cause for the error")
}
}
Explanation:
- We define a struct
UnderlyingError
, to be used as the cause of the primary error. - We define a struct
MyError
that includes amessage
and an optionalsource
, which is another error type. - In the
Error
trait implementation forMyError
, thesource()
method returnsSome(&self.source)
if there's a source error, andNone
otherwise.
Combining Errors
When working with multiple sources of errors, it's often useful to create an error type that can represent any of them. This can be done with an enum
:
use std::fmt;
use std::error::Error;
use std::fs::File;
use std::io::{self, Read};
#[derive(Debug)]
enum AppError {
Io(io::Error),
ParseInt(std::num::ParseIntError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(err) => write!(f, "IO error: {}", err),
AppError::ParseInt(err) => write!(f, "ParseInt error: {}", err),
}
}
}
impl Error for AppError {
fn source(&self) -> Option<&dyn Error> {
match self {
AppError::Io(err) => Some(err),
AppError::ParseInt(err) => Some(err),
}
}
}
fn read_number_from_file(filename: &str) -> Result<i32, AppError> {
let mut file = File::open(filename).map_err(AppError::Io)?;
let mut contents = String::new();
file.read_to_string(&mut contents).map_err(AppError::Io)?;
contents.trim().parse().map_err(AppError::ParseInt)
}
fn main() {
match read_number_from_file("number.txt") {
Ok(number) => println!("Number: {}", number),
Err(err) => {
println!("Error: {}", err);
if let Some(source) = err.source() {
println!("Caused by: {}", source);
}
}
}
}
Explanation:
- The
AppError
enum encapsulates bothio::Error
andstd::num::ParseIntError
. - The
source()
method returns the underlying error depending on which variant is active. - The
read_number_from_file
function uses the?
operator to automatically convert errors fromFile::open
andparse
intoAppError
variants.
thiserror
crate can significantly simplify the process of defining custom error types and implementing the Error
trait. It provides derive macros that automatically generate the necessary boilerplate code.