6

Tglman

 3 years ago
source link: http://www.tglman.com/posts/rust_lib_error_management.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Rust lib error management, multiple enum approach

While building a Rust library, I came across a quite common problem: how to provide errors that are understandable, easy to manage and enough specific to let the user handle the cases.

The usual standard approach for the error structure in Rust is to provide an enum that describes all the possible error cases a library can emit, along with some data to add context for the error, for example:

struct Error {
    kind: ErrorKind,
    data: Box<SomeData>
} 
enum ErrorKind {
    Error1,
    Error2,
}

Or just embedding the data in the enum:

enum Error {
    Error1(DataError1),
    Error2(DataError2),
}

Both these approaches are valid and have advantages and limitations; one advantage being that is easy for a third party to use the library to wrap the error and pass it forward using the try(?) operator.

One limitation is that with one enum to describe all possible errors, the user won't immediately be aware of which errors are actually available for a specific function and which errors can never happen.

One alternative approach I've been testing is by using multiple enums to describe errors, one for each category of error - and that may even boil down to just one specific error for each function. In addition to that, one 'AllErrors' enum (that includes any possible errors of the library) and all the other specific errors can translate to ('AllErrors' implement From all the specific errors).

Here's a quick example:

pub enum AllErrors {
    Error1,
    Error2,
    Error3,
}

pub enum Error1 {
    Error1
}

impl From<Error1> for AllErrors {
    fn from(_: Error1) -> Self {
        AllErrors::Error1
    } 
}

pub enum Error2 {
    Error2,
    Error3
}

impl From<Error2> for AllErrors {
    fn from(error2: Error2) -> Self {
        match error2 {
            Error2::Error2 => AllErrors::Error2,
            Error2::Error3 => AllErrors::Error3,
        }
    } 
}

pub fn operation1() -> Result<(), Error1> {
    Err(Error1::Error1)
}

pub fn operation2() -> Result<(), Error2> {
    Err(Error2::Error2)
}

pub fn operation3() -> Result<(), Error2> {
    Err(Error2::Error3)
}

This solution allows me to expose an API with almost both characteristics: a specific enum for a specific call (where the user can handle the specific issues) and a "catch all" enum to wrap and pass forward the error.

This solution - as it is - has a limitation though: if a user just wants to pass forward any error, they would have to implement all the "From" for all the specific errors my library returns - one possible approach being in theory implementing it as follows:

impl<T:Into<AllErrors>> From<T> for UserError {
    fn from(lib_err:T) -> UserError {
        //....
    }
}

However Rust does not allow that in downstream crates (for reasons I think I understood but I'm not sure how to explain). So in the end a bit more creative solution is needed - and that may look a bit weird at first - but should cover all the use cases I was looking for: having an additional enum in my library with just a possible variant that wraps all the errors, something like:

// Just calling int LE short version of LibError
enum LE<T:Into<AllErrors>>{
    LE(T)
}

to be used as return error of all the public functions of my lib, that they will become:

pub fn operation1() -> Result<(), LE<Error1>> {
    Err(LE::LE(Error1::Error1))
}

pub fn operation2() -> Result<(), LE<Error2>> {
    Err(LE::LE(Error2::Error2))
}

pub fn operation3() -> Result<(), LE<Error2>> {
    Err(LE::LE(Error2::Error3))
}

to allow users to implement:

impl<T:Into<AllErrors>> From<LE<T>> for UserError {
    fn from(lib_err:LE<T>) -> UserError {
        //.... translate the error 
    }
}

In this way a user can have the error bubble up with just the ? operator independently of the specific error and handle only some specific errors directly in the call.

I've created a repository with an example lib project that shows the library and an example implementation to make the case clearer. The lib.rs is the library code and the app.rs is the example app.

This approach may add some complexity on the library implementation side but will definitely make your library more ergonomic for your users.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK