How to define a custom `io::Error` as const?

Is it possible to define a custom std::io::Error as a constant somehow?

So that I don't have to copy&paste the respective IoError::new(....) code everywhere.

use std::io::{Error as IoError, ErrorKind};

pub enum MyError{
    TimedOut,
    Cancelled,
}

impl Error for MyError { }

const MY_ERROR: IoError = IoError::new(ErrorKind::Other, MyError::Cancelled);

...does not work with error:

cannot call non-const fn `std::io::Error::new::<TcpError>` in constants

There is the const_io_error! macro, but it is pub(crate), so I cannot access it :weary:


I tried with lazy_static!, and it works, sort of:

lazy_static! {
    static ref MY_ERROR: IoError = IoError::new(ErrorKind::Other, TcpError::Cancelled);
}

But then, how do I use the lazy_static! constant to actually return an Err value ??? :thinking:

use std::io::{Error as IoError, ErrorKind, Result as IoResult};

pub fn test() -> IoResult<()> {
    Err(MY_ERROR)
}

Error I get is error:

error[E0308]: mismatched types
[...] ^^^^^^^^ expected struct `std::io::Error`, found struct `MY_ERROR`

I don't think there's any publicly exposed ways to constly construct an io::Error, no.

You can only get a reference to your lazy_static, so you'd have to downcast and clone that. Probably I'd just use a helper method or macro (and no lazy_static).

1 Like

Thanks for information!

I cam up with this, but it seems a bit cumbersome:

use std::io::{ErrorKind, Error as IoError, Result as IoResult};

use lazy_static::lazy_static;

lazy_static! {
    pub(crate) static ref ERROR_CANCELLED: ErrorFactory = ErrorFactory::new(ErrorKind::Other, TcpError::Cancelled);
    pub(crate) static ref ERROR_TIMEOUT: ErrorFactory = ErrorFactory::new(ErrorKind::TimedOut, TcpError::TimedOut);
}

#[derive(Copy, Clone)]
pub enum TcpError {
    TimedOut,
    Cancelled,
}

pub(crate) struct ErrorFactory {
    kind: ErrorKind,
    tcp_error: TcpError,
}

impl ErrorFactory {
    fn new(kind: ErrorKind, tcp_error: TcpError) -> Self {
        Self {
            kind,
            tcp_error,
        }
    }

    pub fn error(&self) -> IoError {
        IoError::new(self.kind, self.tcp_error)
    }

    pub fn result<T>(&self) -> IoResult<T> {
        Err(IoError::new(self.kind, self.tcp_error))
    }
}
fn error_test_1() -> IoError {
    ERROR_CANCELLED.error()
}

fn error_test_2() -> IoResult<()> {
    ERROR_CANCELLED.result()
}

Is this the way to go, or is there a slicker solution ???

Depends on what you consider slick I guess... I just meant something straight-forward.

pub mod err {
    pub fn timed_out() -> IoError {
        IoError::new(ErrorKind::TimedOut, TcpError::TimedOut)
    }

    pub fn cancelled() -> IoError {
        IoError::new(ErrorKind::Other, TcpError::Cancelled)
    }
}
fn error_test_1() -> IoError {
    err::cancelled()
}

fn error_test_2() -> IoResult<()> {
    Err(err::timed_out())
}
3 Likes

Oh, I see. Though, a separate function for each type of error doesn't really "scale" nicely :sweat_smile:

Just noticed I can get rid of lazy_static! altogether:

use std::io::{ErrorKind, Error as IoError, Result as IoResult};

pub(crate) const ERROR_CANCELLED: ConstError = ConstError::new(ErrorKind::Other, TcpError::Cancelled);
pub(crate) const ERROR_TIMEOUT: ConstError = ConstError::new(ErrorKind::TimedOut, TcpError::TimedOut);
/* ... */

#[derive(Copy, Clone)]
pub enum TcpError {
    TimedOut,
    Cancelled,
}

pub(crate) struct ConstError {
    kind: ErrorKind,
    tcp_error: TcpError,
}

impl ConstError {
    const fn new(kind: ErrorKind, tcp_error: TcpError) -> Self {
        Self {
            kind,
            tcp_error,
        }
    }

    pub fn to_result<T>(&self) -> IoResult<T> {
        Err(IoError::new(self.kind, self.tcp_error))
    }
}

Matter of preference, you have a separate const that scales at exactly the same rate.

1 Like

I think, in "good" software design, a new class (or struct) or function should be added only if it actually implements a different logic/behavior. Adding new classes or functions, just as a "container" for different data, is considered an anti-pattern. Instead, we ought to parameterize the class, so that the concrete data can be passed in constructor. Then create as many instances as needed, of the same class.

(in other words: better scale up the number of instances, not the number classes/structs or functions)

Rust doesn't have classes, and factories everywhere (or a const per variant) isn't really idiomatic.

Anyway, I eventually realized the actually-idiomatic answer here is just implement From.

4 Likes

It depends. If you've only got 3 or 4 error variants then writing one-line constructors for them is pretty trivial. I see no difference in terms of "good software design" from making it a const instead of a function - both introduce a named "thing" which you can use to produce a common error value.

If it were me, instead of using one common error enum with different variants, I'd probably create separate error types for each case and add an impl From<TimedOut> for std::io::Error so ? can automatically convert my TimedOut error to a std::io::Error. That way you can reuse your TimedOut and Cancelled errors elsewhere or maybe only return Result<_, Cancelled> rather than a more generic Result<_, MyError>, and you get a more precise error chain.

This approach works well in an object-oriented language where inheritance will let you accept multiple different things in your constructor, but Rust isn't your typical OO language. It tends closer towards the functional side of programming where you do operations on data rather than operations with objects.

In functional programming it is very common to have different "plain old data" structs which act as containers for different data. For example, say you are processing data in a way where only certain things will be valid at different times in the operation, then you might create a different struct for each stage which holds only the data that is valid for that stage - you see something similar in the typestate pattern.

More examples of where you might see structs acting as bags of data are builders or when passing around options/configs. Sometimes you might even introduce a temporary struct because you pass the same tuple around 3 or 4 types and want to give it a name.

5 Likes

Yeah, I see. But one difference still would be that each time we need to return a specific custom io::Error, we have to call the corresponding "factory" function, which in turn has to call io::Error::new() in order to create a new instance of the io::Error. Since the "factory" function takes no parameters and the resulting io::Error instance will always be totally equivalent (for a concrete "factory" function), creating a new instance each time seems like an avoidable overhead.

If we actually were able to instantiate our custom io::Error exactly once and have it stored as a const value, then we could simply return the const value "as-is" whenever we need to.

Admittedly, the const Error still would have to be Copy'd when we return it "by-value". But, nonetheless, that should be less overhead and "cleaner" than constructing an entirely new one...

In functional programming it is very common to have different "plain old data" structs which act as containers for different data. For example, say you are processing data in a way where only certain things will be valid at different times in the operation, then you might create a different struct for each stage which holds only the data that is valid for that stage.

Having a separate "container" struct for each stage, which wraps any value that is valid at the specific stage (and never contains "invalid" values for the stage), seems reasonable to me. But not having a separate struct for each valid value, and that multiplied by the number of stages.

Similarly, I wouldn't want to have a separate struct for each error, only for each "class" of errors.

If it were me, instead of using one common error enum with different variants, I'd probably create separate error types for each case and add an impl From<TimedOut> for std::io::Error so ? can automatically convert my TimedOut error to a std::io::Error. That way you can reuse your TimedOut and Cancelled errors elsewhere or maybe only return Result<_, Cancelled> rather than a more generic Result<_, MyError>, and you get a more precise error chain.

Thing is, I want to implement Read and Write which require the result to be a io::Result (i.e. Result<T, io::Error>) and therefore I have to wrap "my" error in an io::Error.

Unfortunately, that won't work out.

A std::io::Error is !Clone because it may contain arbitrary objects in its payload. That means the first time you try to use it you will be moving out of the const/static variable, which is going to be a compile error.

2 Likes