What is the correct way to mix different types / kinds of errors?


#1

I would like to craft a function which does HTTP call and returns either successful result parsed body or an error of one of the following kinds:

  • specific application error if status code is not successful (see marker in code sample below) OR
  • returns hyper::Error if it is underlying transport problem

I have got a function doing this and currently it returns hyper:Error everywhere, because I do not know how to transform it correctly to an error of mixed error type. I have seen examples like this but it smells, in my opinion (what if I will need to mix many more other error types from other crates, eg. from many nested dependencies?)

What is the best practice to solve the problem like this?

    fn connect(&self) -> Result<(), hyper::Error> { // TODO some other different error type should be here?

        let mut core = Core::new()?;
        let client= Client::new(&core.handle());
        ....
        let mut req: Request<hyper::Body> = Request::new(hyper::Method::Post, login_url);
        ....
        let work = client.request(req)
            .and_then(|res| {
                let result = if res.status() == hyper::StatusCode::Created {
                    Adapter::get_body(res.body())
                }
                else {
                    // HERE I WOULD LIKE TO USE MY CUSTOM ERROR TYPE INSTEAD
                    // but use hyper error for now to have something working
                    Box::new(futures::future::err(hyper::Error::TooLarge))
                };
                result
            })
            .map(|res| {
                let login_response: LoginResponse = serde_json::from_str(res.deref()).unwrap();
                println!("{:?}", login_response)
            });

        let result = core.run(work);

        // an example of error handling block,
        // where I intend to print different status messages depending on kind of an error
        // this block will be global in main() function to handle various types of errors from other places
        match result {
            Ok(v) => {
                println!("Result: {:?}", v);
            }
            Err(hyper::error::Error::Io(e)) => {
                println!("IO Error: {}", e);
            }
            Err(e) => { // TODO match on application specific codes
                println!("Error: {}", e);
                println!("Error: {:?}", e.cause().map(|e| {
                    e
                }));
            }
        };

        Ok(())
    }

    fn get_body(body: hyper::Body) -> Box<Future<Item=String, Error=hyper::Error>> {
        let full_body = body.concat2()
            .map(|chunk| {
                let v = chunk.to_vec();
                String::from_utf8_lossy(&v).to_string()
            });
        Box::new(full_body)
    }

#2

The right answer will depend on what you expect the callers to need to do with the error and/or how detailed of an API you’d like to expose. Will they want to discriminate between classes of errors (eg transport/IO vs hyper specific vs …)? Will they just log it and so need only textual content? Can you talk about this a bit?

This is a similar decision you’d need to make when propagating exceptions in Java/Scala from a lower layer.


#3

If I develop a library (in this case), the caller will need to differentiate error types and as much as can be made available information. If it is an application, I would use this rich information to match on various types of errors (unless there is better way) and print corresponding status message and tips how to fix and what to look for. These messages and tips would be different for different causes of errors.


#4

In scala I maintain nested exceptions chain whenever an intermediate function needs to override caught known or unknown exceptions. And then match this chains accordingly to print meaningful messages and tips.


#5

quick_error is quite popular

You specify your own Result and Error type and quick_error takes care of conversion from those specific Errors into your own.


#6

How different is it with error_chain? What is better / easier to combine with futures?


#7

It is also interesting that these nesting of error types is influenced by dependency tree, that is why I think it smells. If lib1 depends on lib2 and lib3, and lib2 depends on lib3, where lib2 and lib3 both depend on hyper, I may end up with errors like:

ErrorLib1(ErrorLib2)
ErrorLib1(ErrorLib3)
ErrorLib2(ErrorLib3)
ErrorLib2(hyper::error)
ErrorLib3(hyper::error)

As a result in order to handle ErrorLib3 on the caller of lib1, I should expect it in 2 places. In order to handle hyper::error I need to expect it in 3 places. This is influenced how libs call each other. If dependency tree is changed error matching should be changed too, although subset of causes remains the same. That is why I think something is not right with this design or i do not understand something?


#8

error_chain is same basic idea. Probably a better choice actually. As for tokio I don’t know. Look at what other tokio projects are using and do that.


#9

I don’t think lib1 should be a union of all errors in its dependencies. Instead, a given API in lib1 should define its own error type. That error type should translate underlying libs’ errors into something of its own. It may collapse several errors in the underlying lib into one. I don’t think lib1 should expose any hyper errors directly as now your API is coupled to hyper - if you decide to use a different http lib in the future it’ll be harder to migrate.