TGLMAN

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.

Posts

Rust lib error management, multiple enum approach
10-04-2021
Tags: rust

Setup Gitlab runner for run ci tests locally
18-01-2020
Tags: self_hosting

Check What You Run
23-12-2019
Tags: sh

Self Hosting Feed Reader Server
02-01-2018
Tags: self_hosting

Rust Lang Love
28-12-2017
Tags: coding, rust

Java Shell Script
13-05-2015
Tags: java, sh

Self Hosted Calendar Server With Radicale
02-05-2015
Tags: self_hosting

New Blog
02-05-2015
Tags: self_hosting

Primitive Boxing Problem
20-11-2012
Tags: java, coding

DDD Query Way
20-10-2012
Tags: java, ddd, coding

Refs

Twitter
Mastodon
Github
StackOverflow
GitLab

Projects

Object Query
URL Freezer