Implementing our own custom Error trait to use in Result enum.

079-feature-image.png
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”:

  1. Trait std::error::Error
  2. Trait std::fmt::Display
  3. Trait std::fmt::Debug
  4. Enum std::result::Result
  5. 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: