Custom Errors
While Rust's standard library provides many error types, creating your own custom error types gives you better control over error messages, allows you to add context, and makes your error handling more structured and maintainable. Custom errors are essential for building robust, user-friendly applications.
Why Custom Errors?
Custom error types provide several benefits:
- Better error messages: Provide context-specific error messages
- Type safety: Compiler ensures you handle all error cases
- Structured errors: Organize errors by domain or module
- Error context: Include additional information about the error
- Error chaining: Preserve error chain for debugging
Creating Custom Error Types
There are two main ways to create custom error types:
- Using enums: For multiple error variants
- Using structs: For errors with additional context
Custom Error with Enum
Enums are great when you have multiple error variants:
use std::fmt;
#[derive(Debug)]
enum MathError {
DivisionByZero,
NegativeNumber,
Overflow,
}
impl fmt::Display for MathError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MathError::DivisionByZero => write!(f, "Cannot divide by zero"),
MathError::NegativeNumber => write!(f, "Number cannot be negative"),
MathError::Overflow => write!(f, "Arithmetic overflow occurred"),
}
}
}
impl std::error::Error for MathError {}
Result
and the ? operator, you need to implement both Display
(for user-facing messages) and Error (for error handling).
Using Custom Error Types
Once you've defined your error type, you can use it in functions:
fn divide(a: i32, b: i32) -> Result<i32, MathError> {
if b == 0 {
Err(MathError::DivisionByZero)
} else {
Ok(a / b)
}
}
fn sqrt(n: i32) -> Result<f64, MathError> {
if n < 0 {
Err(MathError::NegativeNumber)
} else {
Ok((n as f64).sqrt())
}
}
Click Run to execute your code
Custom Error with Struct
Structs are useful when you need to include additional context:
use std::fmt;
#[derive(Debug)]
struct ValidationError {
field: String,
message: String,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Validation error in {}: {}", self.field, self.message)
}
}
impl std::error::Error for ValidationError {}
fn validate_age(age: i32) -> Result<i32, ValidationError> {
if age < 0 {
Err(ValidationError {
field: String::from("age"),
message: String::from("Age cannot be negative"),
})
} else if age > 150 {
Err(ValidationError {
field: String::from("age"),
message: String::from("Age cannot exceed 150"),
})
} else {
Ok(age)
}
}
Click Run to execute your code
Implementing the Error Trait
The Error trait has several optional methods. The minimal
implementation is just an empty impl block:
impl std::error::Error for MathError {}
However, you can implement additional methods for more functionality:
source() - Error Chaining
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_deref()
}
}
source() method allows you to
chain errors, preserving the original error while wrapping it with additional
context. This is very useful for debugging.
Error Conversion
You can convert between error types using the From trait. This allows
the ? operator to automatically convert errors:
impl From<std::num::ParseIntError> for MathError {
fn from(err: std::num::ParseIntError) -> Self {
MathError::ParseError(err.to_string())
}
}
From allows the
? operator to automatically convert between compatible error types,
making error handling more flexible.
Best Practices
- Use enums for variants: When you have multiple related error types
- Use structs for context: When you need to include additional information
- Implement Display: Always provide user-friendly error messages
- Implement Error: Required for use with Result and ? operator
- Add Debug derive: Makes debugging easier with {:?}
- Use descriptive names: Make error types self-documenting
- Include context: Add fields that help understand what went wrong
- Implement From: For automatic error conversion
Common Mistakes
1. Forgetting to implement Display
// Wrong - No Display implementation
#[derive(Debug)]
enum MyError {
SomethingWentWrong,
}
// Correct - Implement Display
#[derive(Debug)]
enum MyError {
SomethingWentWrong,
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Something went wrong")
}
}
2. Not implementing Error trait
// Wrong - Can't use with ? operator
#[derive(Debug)]
struct MyError {
message: String,
}
impl fmt::Display for MyError { /* ... */ }
// Correct - Implement Error trait
impl std::error::Error for MyError {}
3. Generic error messages
// Wrong - Not helpful
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Error") // Too generic!
}
}
// Correct - Include context
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Validation error in {}: {}", self.field, self.message)
}
}
4. Not using Debug derive
// Wrong - Can't use {:?}
enum MyError {
SomethingWentWrong,
}
// Correct - Add Debug derive
#[derive(Debug)]
enum MyError {
SomethingWentWrong,
}
Exercise: Custom Errors Practice
Task: Create custom error types and use them in your functions.
Requirements:
- Create an error enum with multiple variants
- Create an error struct with context fields
- Implement Display and Error traits
- Use your custom errors in functions
Click Run to execute your code
Show Solution
use std::fmt;
#[derive(Debug)]
enum CalculationError {
DivisionByZero,
NegativeSquareRoot,
Overflow,
}
impl fmt::Display for CalculationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CalculationError::DivisionByZero => write!(f, "Division by zero"),
CalculationError::NegativeSquareRoot => write!(f, "Cannot take square root of negative number"),
CalculationError::Overflow => write!(f, "Arithmetic overflow"),
}
}
}
impl std::error::Error for CalculationError {}
fn safe_calculate(a: f64, b: f64) -> Result<f64, CalculationError> {
if b == 0.0 {
return Err(CalculationError::DivisionByZero);
}
let result = a / b;
if result < 0.0 {
return Err(CalculationError::NegativeSquareRoot);
}
Ok(result.sqrt())
}
#[derive(Debug)]
struct ValidationError {
field: String,
value: String,
message: String,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Validation error in {} (value: '{}'): {}",
self.field, self.value, self.message)
}
}
impl std::error::Error for ValidationError {}
fn validate_email(email: &str) -> Result<String, ValidationError> {
if !email.contains('@') {
Err(ValidationError {
field: String::from("email"),
value: email.to_string(),
message: String::from("Email must contain '@'"),
})
} else {
Ok(email.to_string())
}
}
Summary
- Custom errors provide better error messages and structure
- Use enums for multiple error variants
- Use structs for errors with context
- Implement Display trait for user-friendly messages
- Implement Error trait for use with Result
- Add Debug derive for easier debugging
- Implement From for automatic error conversion
- Include context in error messages
- Use descriptive error type names
What's Next?
Congratulations! You've completed the Error Handling module. You now understand how to handle errors in Rust using panics, Results, error propagation, and custom error types. These concepts are fundamental to writing robust Rust programs.
In the next modules, you'll learn about advanced Rust features like traits, generics, lifetimes, and more that build on these error handling foundations.
Enjoying these tutorials?