Best practices for handling IO Errors in libraries

I am trying to create a library that helps with creating IPC Servers and Clients using unix domain sockets.
Obviously there are going to be a lot of IO errors that the application that uses the library is going to be interested in.

I am wondering if it's good practice to treat some very common IO errors separately eg: Connection Refused and WouldBlock so that users can easily check those as opposed to examining io::ErrorKind

Something like this?

use thiserror::Error;

#[derive(Clone, Debug, Error)]
pub enum Error {
    #[error("Connection refused by server")]
    ConnectionRefused,
    #[error("Operation would block")]
    WouldBlock,
    #[error(transparent)]
    IOError(#[from] std::io::Error),
}

Here's a really good article on the subject: Modular Errors in Rust - Sabrina Jewson

I wouldn't bother. How do you decide what's "very common"? Also, if you start doing this, you will start digging down into arbitrary depths of error chains. Your code shouldn't be responsible for that, it's an unnecessary level of complexity.

It also makes the construction of your own error type cumbersome (you always need to check the I/O error, you can't just use ? to perform the automatic From::from() conversion).

The current design also throws away the io::Error instance itself when it's ConnectionRefused or WouldBlock, which is definitely not something you should do.

Just make IoError a single variant with #[from], and call it a day.

I take your point about the drawbacks.

Although about "very common" is easy: WouldBlock for example is very common since I am going to be constructing my library primarily around NonBlocking mode. So the application caller is always supposed to check if an operation WouldBlock and then try again.

I would go a different route; define methods on Error that tell you what your behaviour should be. Something like:

use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("This is not data, it's an ex-parrot!")]
    PiningForTheFjords,
    #[error("Server said to try later")]
    TryLater,
    #[error(transparent)]
    IOError(#[from] std::io::Error),
}

impl Error {
    pub fn should_retry(&self) -> bool {
        use Error::*;
        match self {
            PiningForTheFjords => false,
            TryLater => true,
            IOError(e) => {
                use std::io::ErrorKind::*;
                match e.kind() {
                     WouldBlock => true,
                     TimedOut => true,
                     Interrupted => true,
                     _ => false,
                } 
            }            
        }
    }
}

You'll want something more sophisticated than a bool return type (e.g. something telling you what to wait for before retrying), but this means that complicated code can use match the way should_retry does, to directly understand all the possible error causes, while simple code can use should_retry to tell it what its behaviour "should" be on this error.

4 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.