Generic error enum for lib and propagation review wanted

I would like some feedback if I am using the concept of error propagation correctly.

I am rewriting a library I've made in Python to Rust. The use-case of the library is to expose an easy to work with API for Binance Rest/Websocket functionality.

What I have understood from the Rust idiomatic way about error propagation is that errors are propagated to the caller. I would like to know if the following code and thought process is correct.

I have created a public BinanceConnectError enum

#[derive(Debug)]
pub enum BinanceConnectError {
    HttpError(reqwest::Error),
    JsonError(serde_json::Error),
    Other(String),
}

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

impl From<reqwest::Error> for BinanceConnectError {
    fn from(err: reqwest::Error) -> Self {
        BinanceConnectError::HttpError(err)
    }
}

impl From<serde_json::Error> for BinanceConnectError {
    fn from(err: serde_json::Error) -> Self {
        BinanceConnectError::JsonError(err)
    }
}

impl std::fmt::Display for BinanceConnectError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            BinanceConnectError::HttpError(err) => write!(f, "HTTP error: {}", err),
            BinanceConnectError::JsonError(err) => write!(f, "JSON error: {}", err),
            BinanceConnectError::Other(msg) => write!(f, "Other error: {}", msg),
        }
    }
}


This error enum is used to encapsulate other errors in the library specific error and propagate them to the caller.

use reqwest::blocking::{Client, Response};
use reqwest::StatusCode;
use crate::structs::ListenKey;
use crate::constants;
use crate::error::BinanceConnectError;

pub fn get_listen_key(api_key: String) -> Result<ListenKey, BinanceConnectError> {
    let client: Client = Client::new();
    let endpoint: String = format!("{}{}", constants::BASE_URL_FUTURES_TESTNET, constants::FUTURES_LISTEN_KEY);
    let response: Result<Response, reqwest::Error> = client.post(endpoint).header("X-MBX-APIKEY", api_key).send();

    match response {
        Ok(response) => {
            if response.status() == StatusCode::OK {
                serde_json::from_str(&response.text()?).map_err(BinanceConnectError::JsonError)
            } else {
                Err(BinanceConnectError::Other(format!(
                    "Failed to obtain ListenKey: Non-OK response {:?}",
                    response.status()
                )))
            }
        }
        Err(err) => Err(BinanceConnectError::HttpError(err)),
    }
}

As you can see in the code above, I "caught" 3 different error types and return them as my library generic error. Which allows the following:

    let listen_key_result: Result<ListenKey, BinanceConnectError> =
        binance_connect::rest::futures::get_listen_key("abc".to_string());

    match listen_key_result {
        Ok(listen_key) => println!("listen_key: {:?}", listen_key.key),
        Err(e) => {
            match e {
                BinanceConnectError::HttpError(err) => {
                    println!("HTTP Error: {:?}", err);
                }
                BinanceConnectError::JsonError(err) => {
                    println!("JSON Error: {:?}", err);
                }
                BinanceConnectError::Other(msg) => {
                    println!("Other Error: {}", msg);
                }
            }
        }
    }

The different error types dictate a different approach but that is now the responsibility of the caller implementation.

Looking forward to feedback and insights

You can automate away the impl Error + Display + From<_> boilerplate using thiserror.

In the last snippet, you should probably just use the Display impl instead of re-implementing the formatting mechanism manually.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.