Rust: baby step -- using our own Error trait in Result enum.
Implementing our own custom Error trait
to use in Result enum
.
Rust: baby step – using our own Error trait in Result enum. |
In languages such as Delphi and Python, we can define our own
exceptions, and raise them when we need to. Similarly, in Rust,
we can define our own custom Error traits
to use
in Result<T, E>
.
The primary sources of references for this post are The Rust Standard Library and chapter Error Handling from “the book”:
- Trait std::error::Error
- Trait std::fmt::Display
- Trait std::fmt::Debug
- Enum std::result::Result
-
Chapter 9: Error Handling
section
Recoverable Errors with
Result
in “the book”.
As a Rust beginner, I find The Rust Standard Library very helpful, it is concise and to the point. I feel that it complements “the book” quite nicely.
⓵ Trait std::error::Error
– describes the basic requirements to implement our own Error
trait: we’ve to implement the
Display and
Debug traits.
And this:
Error messages are typically concise lowercase sentences without trailing punctuation:
let err = "NaN".parse::<u32>().unwrap_err(); assert_eq!(err.to_string(), "invalid digit found in string");
⓶ Trait std::fmt::Display – helpful examples on how to implement this trait.
⓷ Trait std::fmt::Debug – I pay special attention to the discussions on derived implementation and manual implementation. I elect to use the later.
⓸ Enum std::result::Result –
concise information on this enum. The main point for me is that
Ok(T)
and Err(E)
are mutually exclusive.
The Error
trait which I’ve in mind is simple: it has a
single string field which is the error message itself. Based on the
info in the above references, I came up with the following:
use std::fmt;
pub struct BhError {
err_msg: String
}
impl BhError {
fn new(msg: &str) -> BhError {
BhError{err_msg: msg.to_string()}
}
}
impl fmt::Debug for BhError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BhError")
.field("err_msg", &self.err_msg)
.finish()
}
}
impl fmt::Display for BhError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f,"{}", self.err_msg)
}
}
⓵ It should display correctly for formats {:?}
and
{:#?}
. ⓶ And to_string()
method should
return the actual error text. To keep the post simple, we will not
write tests, but just write output to the console for manual
verifications. We can test it with:
fn main() {
let err = BhError::new("this is a test error");
println!("{:?}\n", err);
println!("{:#?}\n", err);
println!("{err:?}\n\n");
println!("{}", err.to_string());
}
And the output is what we expect:
behai@hp-pavilion-15:~/rust/errors$ rustc src/example_01.rs
behai@hp-pavilion-15:~/rust/errors$ /home/behai/rust/errors/example_01
BhError { err_msg: "this is a test error" }
BhError {
err_msg: "this is a test error",
}
BhError { err_msg: "this is a test error" }
this is a test error
behai@hp-pavilion-15:~/rust/errors$
Let’s use it in Enum std::result::Result.
Suppose we need a function to return an u32
value on success,
otherwise the BhError
trait. We can write a mock function as
follows:
/// Returns a Result with either a hard-coded ``u32`` value of 234, or
/// ``BhError`` whose messsage is the value of the argument ``error_text``
///
/// # Arguments
///
/// * `raise` - ``true`` to return ``BhError``. false to return an
/// ``u32`` of 234
///
/// * `error_text` - test error text. Blank when ``raise`` is ``false``,
/// some text when ``raise`` is ``true``
///
fn test_error_raising(raise: bool, error_text: &str) -> Result<u32, BhError> {
if raise {
Err(BhError::new(error_text))
} else {
Ok(234)
}
}
Let’s exam the “valid” case first:
fn main() {
//
// "Valid" test, we don't raise error, test_error_raising(...)
// returns hard-coded Ok(234).
//
let result = test_error_raising(false, "");
//
// For my own assertion that I can copy individual pieces of info out
// of the return value.
//
let mut u32_value: u32 = 0;
let mut valid: bool = true;
let mut error_msg = String::from("");
match result {
Ok(value) => u32_value = value,
Err(error) => {
valid = false;
error_msg.push_str(&error.to_string());
}
};
assert_eq!(valid, true);
assert_eq!(u32_value, 234);
assert_eq!(&error_msg, "");
if valid {
println!("1. u32_value = {}", u32_value);
}
else {
println!("1. In error.\nError = {}", error_msg);
}
}
The code is rather verbose, but not at all complicated. I break
the return Result<u32, BhError>
down into
individual relevant variables to prove to myself that I understand
how it works. As expected, the result is 234
for valid:
behai@hp-pavilion-15:~/rust/errors$ rustc src/example_02.rs
behai@hp-pavilion-15:~/rust/errors$ /home/behai/rust/errors/example_02
1. u32_value = 234
behai@hp-pavilion-15:~/rust/errors$
Next, the “invalid” case:
fn main() {
//
// "Invalid" test, we raise error, test_error_raising(...)
// returns Err(BhError::new("behai raises test error")).
//
let result = test_error_raising(true, "behai raises test error");
let mut u32_value: u32 = 0;
let mut valid: bool = true;
let mut error_msg = String::from("");
match result {
Ok(value) => u32_value = value,
Err(error) => {
valid = false;
error_msg.push_str(&error.to_string());
}
};
assert_eq!(valid, false);
assert_eq!(u32_value, 0);
assert_eq!(&error_msg, "behai raises test error");
if valid {
println!("2. u32_value = {}", u32_value);
}
else {
println!("2. In error.\nError = {}", error_msg);
}
}
The structure and the flow of the code are identical to the above “valid” case. And we get the error printed out as expected:
behai@hp-pavilion-15:~/rust/errors$ rustc src/example_03.rs
behai@hp-pavilion-15:~/rust/errors$ /home/behai/rust/errors/example_03
2. In error.
Error = behai raises test error
behai@hp-pavilion-15:~/rust/errors$
I’m very happy when understood how the Error
trait works.
The Rust Standard Library
is an essential reference. Overall, I’m very happy that
I’m able to complete this post. I hope it is helpful and relevant to somebody…
Thank you for reading and stay safe as always.
✿✿✿
Feature image source: