Implementation of custom Error types

This is a follow-up question of this one: Improving my error handling using custom error types

When just into Rust I spammed .unwrap() everywhere. Then I started using Result<(), String> and now I want to use custom error types to handle errors in the best way.

I have different modules (http, tradealgorithm, and api). I have given each module his own Error-type.

The code is slimmed down:

http.rs

// Process the incoming stream.
pub async fn process(stream: &mut TcpStream, psql: Psql, routes: &std::vec::Vec<Route>, api: Api) -> Result<(), http::Error> {
    //...
}


// ...

pub async fn validate_session_token(session_token: &str, table: DBTable<'_>, psql: Psql) -> Result<bool, http::Error> {
    // Execute query.
    let query = //...

    match query {
        Ok(q) => {
            //...
        },
        Err(e) => {
           return Err(http::Error::DatabaseError(format!("{}", e)));
        }
    };

     //...
}


// Error type for the HTTPServer.
#[derive(Debug)]
pub enum Error {
    HttpServerError(String),
    ParseError(String),
    DatabaseError(String),
    RequestError(String),
    WebsocketError(String),
    StreamError(String),
    TradeAlgorithmError(String),
}

impl std::error::Error for Error {}

impl From<serde_json::Error> for Error {
    fn from(e: serde_json::Error) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl From<reqwest::Error> for Error {
    fn from(e: reqwest::Error) -> Self {
        Error::RequestError(e.to_string())
    }
}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::StreamError(e.to_string())
    }
}

impl From<url::ParseError> for Error {
    fn from(e: url::ParseError) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl From<regex::Error> for Error {
    fn from(e: regex::Error) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl From<tradealgorithm::Error> for Error {
    fn from(e: tradealgorithm::Error) -> Self {
        Error::TradeAlgorithmError(e.to_string())
    }
}

impl From<api::Error> for Error {
    fn from(e: api::Error) -> Self {
        Error::RequestError(e.to_string())
    }
}


impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Error::HttpServerError(error_msg) => write!(f, "\x1b[31m[Error] HttpServer - HttpServerError: {}\x1b[0m", error_msg),
            Error::ParseError(error_msg) => write!(f, "\x1b[31m[Error] HttpServer - ParseError: {}\x1b[0m", error_msg),
            Error::DatabaseError(error_msg) => write!(f, "\x1b[31m[Error] HttpServer - DatabaseError: {}\x1b[0m", error_msg),
            Error::RequestError(error_msg) => write!(f, "\x1b[31m[Error] HttpServer - RequestError: {}\x1b[0m", error_msg),
            Error::WebsocketError(error_msg) => write!(f, "\x1b[31m[Error] HttpServer - WebsocketError: {}\x1b[0m", error_msg),
            Error::StreamError(error_msg) => write!(f, "\x1b[31m[Error] HttpServer - StreamError: {}\x1b[0m", error_msg),
            Error::TradeAlgorithmError(error_msg) => write!(f, "\x1b[31m[Error] HttpServer - TradeAlgorithmError: {}\x1b[0m", error_msg),
        }
    }
}

Here you can see that the functions now return Result<(), http::Error>. And example how an error is returned:

return Err(http::Error::DatabaseError(format!("{}", e)));

api.rs

#[async_trait]
pub trait ExchangeAPI : Send + Sync {
    fn new(rest_api_url: &str, ws_api_url: &str, ws_stream_url: &str, api_key: &str, api_secret: &str) -> Self where Self: Sized;
    async fn ping(&self) -> bool;
    async fn order(&self, params: &mut std::collections::HashMap<String, String>) -> Result<String, api::Error>;
    // ...
}

// Error type for ExchangeAPI.
#[derive(Debug)]
pub enum Error {
    ExchangeAPIError(String),
    RequestError(String),
    ParseError(String),
    DatabaseError(String),
}

impl std::error::Error for Error {}
impl From<sha1::digest::InvalidLength> for Error {
    fn from(e: sha1::digest::InvalidLength) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl From<serde_json::Error> for Error {
    fn from(e: serde_json::Error) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl From<reqwest::Error> for Error {
    fn from(e: reqwest::Error) -> Self {
        Error::RequestError(e.to_string())
    }
}

impl From<url::ParseError> for Error {
    fn from(e: url::ParseError) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Error::ExchangeAPIError(error_msg) => write!(f, "\x1b[31m[Error] ExchangeAPI - ExchangeAPIError: {}\x1b[0m", error_msg),
            Error::ParseError(error_msg) => write!(f, "\x1b[31m[Error] ExchangeAPI - ParseError: {}\x1b[0m", error_msg),
            Error::DatabaseError(error_msg) => write!(f, "\x1b[31m[Error] ExchangeAPI - DatabaseError: {}\x1b[0m", error_msg),
            Error::RequestError(error_msg) => write!(f, "\x1b[31m[Error] ExchangeAPI - RequestError: {}\x1b[0m", error_msg),
        }
    }
}

tradealgorithm.rs

// TradeAlgorithm is used to interpet the Python files in which you
// can create your custom trading algorithm. Each algorithm takes a candlestick
// as parameter.

// A TradeAlgorithm has a Python-script containing the algorithm we want to test.
// Each algorithm gets a specific amount of funds assigned to play with.
#[derive(Clone)]
pub struct TradeAlgorithm {
    pub description: String,
    pub id: String,
    pub start_funds: f64,
    pub interval: String,
    pub run_every_sec: i32,
}

impl TradeAlgorithm {
    //Create a new trading algorithm and insert into database.
    pub async fn new(id: String, description: String, funds: f64, interval: String, run_every_sec: i32, user_id: i32, psql: Psql) -> Result<Self, tradealgorithm::Error> {
        // ...
    }

    // Delete algorithm from database.
    pub async fn delete(&self, psql: Psql) -> Result<(), tradealgorithm::Error> {
        let query = psql.lock().await
           .query("
                DELETE FROM algorithms
                WHERE
                    id = $1
            ", &[&self.id]).await;

        match query {
            Ok(_) => {
                std::fs::remove_file(format!("trading_algos/{}.py", self.id))?;
                Ok(())
            },
            Err(e) => {
                Err(tradealgorithm::Error::DatabaseError(format!("Couldn't delete algorithm: {}", e)))
            }
        }
    }

    // Retrieve trading algorithm with 'id' from database.'
    pub async fn get(id: String, psql: Psql) -> Result<Self, tradealgorithm::Error> {
       // ...
    }

    // Get current funds from this algorithm. We sum the total amount from the history.
    // On success returns a tuple (f64, f64) -> (USDT, BTC)
    pub async fn get_current_funds(&self, psql: Psql) -> Result<(f64, f64), tradealgorithm::Error> {
        //...
    }

    // ...

}

// Error type for TradeAlgorithm.
#[derive(Debug)]
pub enum Error {
    APIError(String),
    AlgorithmError(String),
    ParseError(String),
    DatabaseError(String),
    PythonCodeError(String),
}

impl std::error::Error for Error {}

impl From<serde_json::Error> for Error {
    fn from(e: serde_json::Error) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl From<reqwest::Error> for Error {
    fn from(e: reqwest::Error) -> Self {
        Error::APIError(e.to_string())
    }
}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::AlgorithmError(e.to_string())
    }
}

impl From<url::ParseError> for Error {
    fn from(e: url::ParseError) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl From<api::Error> for Error {
    fn from(e: api::Error) -> Self {
        Error::APIError(e.to_string())
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Error::APIError(error_msg) => write!(f, "\x1b[31m[Error] TradeAlgorithm - APIError: {}\x1b[0m", error_msg),
            Error::ParseError(error_msg) => write!(f, "\x1b[31m[Error] TradeAlgorithm - ParseError: {}\x1b[0m", error_msg),
            Error::DatabaseError(error_msg) => write!(f, "\x1b[31m[Error] TradeAlgorithm - DatabaseError: {}\x1b[0m", error_msg),
            Error::PythonCodeError(error_msg) => write!(f, "\x1b[31m[Error] TradeAlgorithm - PythonCodeError: {}\x1b[0m", error_msg),
            Error::AlgorithmError(error_msg) => write!(f, "\x1b[31m[Error] TradeAlgorithm - AlgorithmError: {}\x1b[0m", error_msg),
        }
    }
}

Notes:

  • The functions in a module return their own error-type.
  • The different Error-enums from different modules can have the same item. E.g DatabaseError(String).
  • The different Error-enums implement each other with From<...> so I can use the ?-operator.
  • All the Error-enums have to e.g use impl From<serde_json::Error> for Error because serde_json is used in all of them.
  • The Error-enum is always declared on the bottom of the file to keep the essence of the code at top.

Questions:

  • Is this approach ok? Clean, efficient and elegant?
  • I notice a lot of duplicate code, can this be written more efficient?
  • Can this be written more elegant? return Err(http::Error::DatabaseError(format!("{}", e))); -> Get rid of format!()?
  • Other tips?

I haven't read your recent error posts in depth, so hopefully this general advice is applicable.

This is a good article on writing ideal error types, for some definition of ideal. You basically manually implement backtraces, but on a "logical task" granularity (which is subjective).

It usually involves a ton of boilerplate and in my personal projects (where I don't have to worry about a stable API others are consuming), I tend to get some distance down the ideal path in an iterative fashion.[1] Sort of like your overall Rust arc, but I always start with a Result<_, Something>.


This one's answerable regardless of everything else though:

-return Err(http::Error::DatabaseError(format!("{}", e)));
+return Err(http::Error::DatabaseError(e.to_string()));

(If you can Display it, you can ToString it.)

Or possibly just a .into() somewhere (on e or DatabaseError or Err) depending on details I didn't look up.


  1. Also I generally have some way to compose the errors that isn't an IIFE ↩ī¸Ž

2 Likes

So something like this would be better?

#[derive(Debug)]
pub struct Error {
    pub error_type: ErrorType,
}

#[derive(Debug)]
pub enum ErrorType {
    HttpServerError(String),
    ParseError(String),
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self.kind {
            ErrorType::HttpServerError(e) => Some(e),
            ErrorType::ParseError(e) => Some(e),
        }
    }
}

But I don't see the article explaining on how to effeciently implement the error-types of other crates like serde_json or reqwest.

// How to do this more efficient?
impl From<serde_json::Error> for Error {
    fn from(e: serde_json::Error) -> Self {
        Error::ParseError(e.to_string())
    }
}

impl From<reqwest::Error> for Error {
    fn from(e: reqwest::Error) -> Self {
        Error::RequestError(e.to_string())
    }
}

//...