Different types of Errors for Result::Err and making my Own Errors

I was making a function That uses reqwest, and I wanted it to return an error. However the error could be two different Types of errors, reqwest::Error or serde_json::Error. Now I'm certain this is probably one of the most common things we have to do, but I still really haven't gotten the hang of it, and I mostly tried to make do with Option instead of Result, or String results till now. This time I made a go at making my own Error in a simple way

#[derive(Debug)]
pub enum Error {
    ReqwestError(reqwest::Error),
    SerdeJsonError(serde_json::Error),
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        match self {
            Error::ReqwestError(err) => write!(f, "{}", err),
            Error::SerdeJsonError(err) => write!(f, "{}", err),
        }
    }
}

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

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

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

pub async fn get_json(url: &str) -> Result<serde_json::Value, Error> {
    Ok(serde_json::from_str(
        &(reqwest::get(url).await?.text().await?),
    )?)
}

Now this can encapsulate both types of errors, and since it implements From<> for both errors, '?' works fine too

Now I went and looked at reqwest's Error and it seems to be implemented differently with a struct with various fields.

My general question is, What is the best way to deal with these things? to merge multiple errors and make my own errors? I'd prefer not to use the Error trait object, as it Might cause limitations elsewhere, but if that's the way to go about it please mention it.

1 Like

check out the thiserror library for custom error types and anyhow's Result type.

1 Like

It really depends on what the caller wants to do with your Error. Is it enough to know it was an HTTP error or a deserialisation error or does it need to know details for each type of the error?

From what I've encountered so far, wrapping reqwest/serde errors in my error structures is fine until I need to use them in tests, it can get quite tricky to generate Error with embedded reqwest error. So in some cases it makes sense not to wrap a low level error. It makes it easier to mock or even swap the low level library without the caller of your module noticing.

If you want to expose the low level error, you can save typing by using thiserror, this is equivalent to your code above:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("HTTP Request failed: {}", .0)]
    ReqwestError(#[from] reqwest::Error),

    #[error("Serde failed")]
    SerdeJsonError(#[from] serde_json::Error),
}
1 Like

The question isn't really specific to this use case, I'm asking how to work with errors in general, So I'd like to preserve as much information as possible.
Isn't there any particular recommended way of doing this?

This looks useful, I'll look into it. Thanks

What you choose depends on the use case. Sometimes it's more useful to wrap errors in an enum like you did, sometimes it's more natural to to convert lower level errors to a richer enum (e.g. instead of a single reqwest error you'd split it into connection error and transfer error) and in some cases it makes sense to just return Box<dyn std::error::Error>.

When I think about errors I try to see them through the caller's eyes. How much detail is necessary? Can that detail be just attached to the error using source field or does it need to be part of the error itself? What is the user experience, how much jumping through hoops is needed to skip through noise and get useful information back? What is needed/useful for the caller to make a decision how to handle the error -- in your case perhaps json errors would not result in the caller trying to re-download the resource, 404 is not worth retrying either but 5xx maybe is.

I tend to start with a simple thiserror wrapper and see where it leads. There were cases when I replaced Result with an Option since logging the error was enough.

This forum is a great source of information but there are plenty of blogs out there, one of the posts I read recently was https://nick.groenen.me/posts/rust-error-handling/

Thanks for the detailed response. That is generally what I was looking for, some advice on how to approach it and decide.

I like the anyhow crate. Then I can write something like this:

let (ws_stream, _response) = tokio_tungstenite::client_async_tls(request, stream).await
    .context("client_async_tls failed.")?;

and get errors reported like this:

Error: TCP connection failed.

Caused by:
    Connection refused (os error 111)
1 Like

As already written it really depends how much details about errors you'd like to pass on, which is in reverse relationship to complexity/work needed for errors implementation. My current rule of thumb is anyhow for bins, thiserror for libs, but it's very approximate. If you create lib, where very detailed errors are needed (like for instance a parser) I would probably consider implementing custom error.

1 Like

Indeed. Sometime I think, despite what people say, using .unwrap() is the perfect solution.

Then my program fails fast on catastrophic error, I get a message in my logs telling me exactly what happened and in which source file/line number, I don't need to do any extra work to get it :slight_smile:

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.