Nested error_chain


#1

Hey.

I’m looking for arguments on how to design my error_chain errors within my single crate.

The general question is, should I use one mod errors { error_chain!{} } for all errors that can occour within the crate or do I create nested mod errors {} for sub modules where appropiate?

In my example I have multiple modules called encryption, decryption and key. For the key module, there are two ways to decode a EncryptionPublicKey from an human readable format. Each way of decoding can fail due to multiple reasons, e.g. illegal characters or wrong key format version.

I imagine two variants to deal with these errors.

Variant 1: Use one big error_chain!{ CouldNotDecodeKey, WrongKeyVersion, IllegalCharacterInKey }
and chain the errors.
Err(ErrorKind::IllegalCharacterInKey.into()).chain_err(|| ErrorKind::CouldNotDecodeKey) ...
This is easy to code but it is hard to tell which errors can occour from calling a certain function.

Variant 2: Use one small error_chain! for each sub functionality. (The related question is, how far should this been taken, should I use seperate error_chain!s for the two different ways of decoding a key?)

mod keys {
    mod hex {
        mod errors {
            error_chain!{ errors { WrongLength {} IllegalCharacter {} } }
        }
        use errors::*;
    }
    mod kb {
        mod errors {
            error_chain!{ errors { UnsupportedKeyVersion {} WrongType {} } }
        }
        use errors::*;
    }
    mod errors {
        error_chain!{ link { NotAHexKey(super::hex::errors::Error, super::hex::errors::ErrorKind)
                             NotAKbKey(super::kb::errors::Error, super::kb::errors::ErrorKind)
        } }
    }
    use errors::*;
}
mod errors {
    error_chain! {
        link { KeyDecodeError( super::key::errors::Error, ....) }
   }
}
use errors::*;

With this method it is more complicated to think about which Result is currently in scope. It is also more complicated to write. But it allows for much better matching of error causes for users of my library.

Is there a better way of doing the error handling or any arguments that I have not thought of?

Thank you,
Daniel


#2

I usually use a single error type for the entire crate. If your crate is so large that you need multiple error chains then that’s probably an indicator that you can break the crate up into multiple sub-crates.

What I usually do is return the actual error (ErrorKind::IllegalCharacterInKey), then chain on a more human-readable message at higher levels. So your example may turn into something like this:

fn decode_key() -> Result<()> {
    read_key().chain_err(|| "Could not decode key")?;

    // do other stuff to decode the key
}

fn read_key() -> Result<()> {
    Err(ErrorKind::IllegalCharacterInKey.into())
}

I’d argue that CouldNotDecodeKey isn’t really an error, it’s more of a higher level explanation of what happened, which is why I’ve used .chain_err(|| "some error message")? instead of an actual error variant.

Another thing you may want to do is look at existing users of error chain and read through their source code to see how they’ve done things (this list may be useful).


#3

I’m fascinated by this idea. I’m curious what kind of baggage it drags along with it.

How would you match against an IllegalCharacterInKey error returned from decode_key? Or is chain_err reserved for code nearest the UI/IO boundary before informing the user/logging?

match decode_key() {
    Ok(_) => {}
    Err(Error(ErrorKind::??(ErrorKind::IllegalCharacterInKey(_))) => {}   // what kind ?? does chain_err use?
    _ => {}
}

Do you also restrict a given function to only use chain_err or only use ErrorKind.into()? Or do you freely mix them as desired/needed in a single function?


#4

You can use them any way you want, I only put them that way because it was an example.

Another really useful thing is to use the bail!() macro as short-hand for returning an error. bail!(ErrorKind::IllegalCharacterInKey('a')) would expand into something like return Err(ErrorKind::IllegalCharacterInKey('a').into())

Typically if you want to see what kind of error you got you’ll use the some_error.kind() method to get the associated ErrorKind for matching. I hadn’t thought about the effects chaining has on the error kind though. Mainly I’ll chain a message on fairly high up so either the error cause was pretty evident and I’ll either fall back to something else or bubble the erro rup, or I’ll end up printing the chained error to the user as a form of backtrace. So something like this:

Error: Key decoding failed, 
    caused by: unable to read key file
    caused by: illegal character in key, "a"

#5

I looked (randomly) for other use cases:

  • metadeps: small lib (not representative)
  • dotenv: Uses few error types, no chain_err(), each error is bubbled up unmodified.
  • xargo: Only uses ErrorKind::Msg()
  • trust-dns: Uses multiple error_chains that are linked. (https://github.com/bluejekyll/trust-dns/tree/master/client/src/error , e.g. client_errors is linked to encode_errors and decode_errors)
  • jsonwebtoken: All errors are defined together like InvalidToken and ExpiredSignature. No chain_error(), each error is bubbled up unmodified.

I think I’ll try variant 1. Seems like I’m not the only one who uses this approach and it may be better for libraries that will be used by someone else (and probably a GUI). It allows for much better prediction of which errors can occour.

I have to think about it if it is possible to define a set of possible errors for each function by using the type system and not a doc explanation.


#6

One Problem occoured while trying the many error_chains approach: The resulting ErrorKind tree exposes information about the internal private function stucture as part of the public API.

For example I have a private fn hex_to_bytes() -> Result where the result has two foreign_links { Utf8Error(::std::str::Utf8Error);ParseIntError(::std::num::ParseIntError); . The error_chain for both of my decode_key() variants would have to add a link{ hex_to_bytes_Result } ErrorKind variant to deal with the errors of the private function hex_to_bytes(). But this will effectively remove my ability to refactor in the future without changing the ErrorKind tree.

Also error_chains cannot be linked in cycles. So I am kind of restricted in my function flow. (I could use a ErrorKind::Msg error for all internal errors and only chain a definite ErrorKind variant on public facing functions. But that would still end up restricting me in writing handy wrappers that combine multiple of my public facing functions that would still have their own error_chains. (e.g. a wrapper that decodes a key and encrypts afterwards )).

Hm.


#7

This is a possible extension (see comments in source). But the major drawback is, that it does not typecheck and may cause panics during runtime when the auther isn’t very carefully. So Bad.

An alternative would be a compiler plugin that statically analyzes all code paths and gives an overview which ErrorKinds may be returned by which function.