Skip to content

Instantly share code, notes, and snippets.

@frasertweedale
Last active January 29, 2022 19:26
Show Gist options
  • Save frasertweedale/11e405f6d76bd0cc8409d0e41836bc9f to your computer and use it in GitHub Desktop.
Save frasertweedale/11e405f6d76bd0cc8409d0e41836bc9f to your computer and use it in GitHub Desktop.
classy errors in rust
/* Composable errors via traits
A major challenge (among several) in error handling is how to deal with
disjoint error types produced by the libraries you use (as well as by
your own program) in a consistent and ergonomic way. Rust's `?` operator
improves the ergonomics of error handling, but it can only be used with
a single error type throughout a single function. Libraries define their
own error types and they don't know about each other, nor about the calling
program. So if we want to deal with errors from different libraries as
a single type (an sum or enum of the different underlying error types), we
have to "wrap" every call to each library routine to "lift" its native
error type into our custom, composite error type.
Or do we? Rust has parametric polymorphism (generics) and traits.
If each library expresses its error constructors via a trait, then
the error type can be decided by the *user* of the library. The
only constraint is that the error type implements that library's
error trait, so that the library can construct the error values.
If all libraries embrace this approach, then their errors are composable
with errors of all other libraries. The programmer of the application
only needs to define the composite error type, and implement the error
trait of each library. Thanks to trait default implementations, this
means implementing one trivial function for each trait.
If not all libraries embrace this approach, for libraries that do not
express their errors abstractly you will still need to do the "lifting"
of those error values into the composite error type yourself. But you
were going to have to do that anyway, so that is not really a drawback.
To refactor a library to use this approach is straightforward, assuming
the library already expresses its errors as a sum/enum:
- define the trait
- one function that takes a value of the library's error enum
(I name it "this" but the name doesn't matter)
- one function for each constructor of the enum, with a
default implementation that makes use of "this"
- implement the trait for the library's error enum
- refactor functions that return `Result<X, LibraryError>`
- replace `LibraryError` with generic error type having the new trait
as a constraint
- replace error enum constructors with calls to the corresponding
trait method
That's it. This change is backwards compatible, with one caveat: the
error type may now be ambiguous ("cannot infer type" compiler error)
for some applications and hence require fixing (monomorphisation) via
a type annotation, helper function or a `match` on the error value.
This only arises if the application code never referred to the concrete
error type, e.g. it used the value via the `Debug` or `Display` trait,
or didn't use the value at all.
In the example below, assume the MIME and Crypto code are from completely
different, independent libraries important by the main application code.
(I don't know anything about Rust modules/packages, and in any case its
probably easier to convey this technique if you can see it all in a single
file).
*/
/* MIME LIBRARY CODE */
#[derive(Debug)]
pub enum MimeError {
ParseError(String),
TransferEncodingUnknown(String),
CharsetUnknown(String),
}
pub trait AsMimeError: Sized {
fn this(MimeError) -> Self;
fn parse_error(s: String) -> Self {
AsMimeError::this(MimeError::ParseError(s))
}
fn transfer_encoding_unknown(s: String) -> Self {
AsMimeError::this(MimeError::TransferEncodingUnknown(s))
}
fn charset_unknown(s: String) -> Self {
AsMimeError::this(MimeError::CharsetUnknown(s))
}
}
impl AsMimeError for MimeError {
fn this(e: MimeError) -> Self {
e
}
}
pub struct MimeMessage {
headers: String, // bogus, should be a map
body: String,
}
pub fn mime_decode<E: AsMimeError>(_s: String) -> Result<MimeMessage, E> {
Err(AsMimeError::parse_error(String::from("Failed to parse")))
}
/* CRYPTO LIBRARY CODE */
#[derive(Debug)]
pub enum CryptoError {
DecryptError(String),
VerifyError(String),
}
pub trait AsCryptoError: Sized {
fn this(CryptoError) -> Self;
fn decrypt_error(s: String) -> Self {
AsCryptoError::this(CryptoError::DecryptError(s))
}
fn verify_error(s: String) -> Self {
AsCryptoError::this(CryptoError::VerifyError(s))
}
}
impl AsCryptoError for CryptoError {
fn this(e: CryptoError) -> Self {
e
}
}
/* verify the signature; returns the verified message, or an error */
pub fn verify<E: AsCryptoError>(_msg: String, _sig: String)
-> Result<String, E> {
Err(AsCryptoError::verify_error(String::from("Failed to verify")))
}
/* APPLICATION CODE */
/* Here we have a sum type that captures all the possible
* errors produced either within our application code,
* or by libraries we are using.
*/
#[derive(Debug)]
pub enum AppError {
CryptoError(CryptoError),
MimeError(MimeError),
}
/* Implement the AsFooError traits for our composite error type */
impl AsMimeError for AppError {
fn this(e: MimeError) -> Self {
AppError::MimeError(e)
}
}
impl AsCryptoError for AppError {
fn this(e: CryptoError) -> Self {
AppError::CryptoError(e)
}
}
fn main() {
println!("Hello world");
match go() {
Ok(_r) => println!("We good"),
Err(e) => println!("Whups: {:?}", e),
}
}
/* go() uses two libraries that know nothing of each other,
* and nothing of our application. But because both libraries
* express their error constructors via a trait, we are able
* to define our AppError composite type, and both libraries
* return errors of that type.
*/
fn go() -> Result<String, AppError> {
let s = String::from("stdin"); // bogus but you get the idea
let msg = mime_decode(s)?;
let opt_sig = check_signed(&msg);
match opt_sig {
None => Ok(msg.body),
Some(sig) => verify(msg.body, sig),
}
}
/* return signature if message signed */
fn check_signed(_msg: &MimeMessage) -> Option<String> {
None
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment