[crazy] Exception-like constructs for Result


#1

Summary

Language support to improve Result-based error handling.

Motivation

Error handling is painful in most languages:

  • integer error codes are simple, but offer little safety. The possible set of values must be documented separately (and kept in sync with the code!) for each function. Invalid usage is hard to detect, forcing people to update their code on API change is even harder.
  • exceptions can be nice, but the implementations of other languages are lacking in key areas (anything can throw at any time, “invisible magic goto”).
  • Result is awesome but verbose, especially with multiple error types.

A Rusty error handling scheme would need to

  • represent all possible error cases as part of the function signature.
  • ensure the programmer can keep track of control flow easily. Code that throws should look like code that throws.
  • handling errors at the appropriate level should be the easiest option.
  • make it easy to opt out
  • ideally, no overhead compared to proper error handling in any other language.

Alternatives

  • Do nothing.
  • Standardize on Result<T, Errno>, with a global repository of error codes and a reserved set of values for private use. (2^32 errors should be enough for everyone!) The drawback of course is the need to coordinate error codes. This is the most familiar option for C folks.
  • Variadic generics: Result<T, Err<E1, ...>>, Result<T, Err1, ...>. This should be the ideal solution, but for the common case of error handling, it might not improve usablity as much as dedicated language features.

Drawbacks

  • More language.
  • Assumes libcore.
  • This is essentially a “poor man’s variadics”, and may become obsolete.

Detailed design

throws

fn foo() -> T throws Foo, Bar { ... }

is desugared into something like

enum Magic {
    MagicFoo(Foo),
    MagicBar(Bar)
}
fn foo() -> Result<T,Magic> { ... }

The generated enum and its variants are anonymous and cannot be directly referenced from code. The compiler is free to choose the representation, for example to collect all errors in a crate into a single enum, or even across crates.

throw

The throw statement generates errors:

throw *expr*;

desugars into something like

return Err(MagicFoo(*expr*));

try

The try! macro is promoted to an expression:

let foo = try *expr*;

desugars into something like

let foo = match *expr* { 
    Ok(k) => k, 
    Err(MagicFoo(e)) => throw e,
    ...
};

Again, the expression’s error list must be a subset of the surrounding function’s error list.

catch

The catch expression deconstructs errors:

catch *expr* {
    Ok(k) => ...,
    e: Foo => ...,
}

desugars into something like

match *expr* {
    Ok(k) => ...,
    Err(MagicFoo(e)) => ...,
    ...
}

Unresolved questions

  • the syntax and semantics of catch. For example, catch (try foo()) can be surprising.
  • whether variadic generics (or anything else) would be better suited for error handling
  • nothing is set in stone, really

Code example

Stolen from http://rustbyexample.com/result.html:

mod checked {
    struct DivisionByZero
    struct NegativeLogarithm;
    struct NegativeSquareRoot(f64);

    pub fn div(x: f64, y: f64) -> f64 throws DivisionByZero {
        if y == 0.0 {
            throw DivisionByZero;
        } else {
            Ok(x / y)
        }
    }

    pub fn sqrt(x: f64) -> f64 throws NegativeSquareRoot {
        if x < 0.0 {
            throw NegativeSquareRoot(x);
        } else {
            Ok(x.sqrt())
        }
    }

    pub fn ln(x: f64) -> f64 throws NegativeLogarithm {
        if x < 0.0 {
            throw NegativeLogarithm;
        } else {
            Ok(x.ln())
        }
    }
}

// `op(x, y)` === `sqrt(ln(x / y))`
fn op(x: f64, y: f64) -> f64 {
    // This was a match pyramid
    let compute = |x,y| {
        use checked::{div,ln,sqrt};
        try sqrt(try ln(try div(x, y)))
    }

    catch compute(x, y) {
        Ok(result) => result,
        checked::DivisionByZero => fail!(),
        checked::NegativeLogarithm => fail!(),
        e: checked::NegativeSquareRoot => fail!("Can't compute the square root of {}", e.0),
    }
}

#2

You may be interested in. : )


#3

another execption like idea

http://discuss.rust-lang.org/t/maybe-better-nicer-error-handling/756/1


#4

I may be missing something in this area. But one problem seems to be that each package has a different result type. I’ve been writing an RSS reader, and I can get errors of type IoError, HTTPError, and XMLError. How do I put those into one Result type? As far as I know, those are all unrelated types.


#5

The docs for std::error gives an example that is exactly what @John_Nagle described.


#6

Typically, what I’ve seen is to define an enum that wraps each of the possible error types. It then implements FromError to wrap each of the sub errors in to the modules local error type. The local error type also usually includes errors meaningful for that module.

Once this is done, try!(...) will automatically wrap the error returns into the local error type. It seems to work fairly well, although writing the wrappers and all of the FromError impls can get a little verbose. Maybe this convention could be standardized to make into a general macro.

A slightly more verbose example. In a module I’ve written to control lvm, I can get an IoError, a possible error from running a command (a std::io::process::ProcessExit), or an error in my code, which just results right now in a message. So, I define:

#[derive(Show)]
pub enum Error {
    Io(io::IoError),
    Command(process::ProcessExit),
    Message(String),
}

as well as

impl Error {
    fn message(text: &str) -> Error {
        Error::Message(text.to_string())
    }
}

impl error::FromError<io::IoError> for Error {
    fn from_error(err: io::IoError) -> Error {
        Error::Io(err)
    }
}

To wrap each of these. Then a function that returns a Result<..., Error> can use try!(...) inside of it, and it will wrap and return any error returned by that.


#7

I’ve done that. it works fine. But it’s verbose, as you can see below. Python accomplishes the same result with far less verbiage. How about a #derive which generates all those boilerplate from_error functions automatically from the enumeration type?

In addition, a well-defined error hierarchy would be helpful. Rust errors are not classified in any useful way. Python has a standard exception hierarchy, with errors induced by external conditions (files, network, etc.) being subclasses of EnvironmentError. New exceptions are added as subclasses of the most relevant exception type. This provides a clear distinction between program bugs and external data issues. I’m not clear on whether this would be possible in Rust.

Having to write the code below over and over in many programs seems wasteful.

//
//  Return type and its error handling
//
pub type FeedResult<T> = Result<T, FeedError>;

/// A set of errors that can occur handling RSS or Atom feeds.
#[derive(Debug, PartialEq, Clone)] // ???
pub enum FeedError {
    /// Error detected at the I/O level
    FeedIoError(IoError),
    /// Error detected at the HTTP level
    FeedHTTPError(hyper::HttpError),
    /// Error detected at the XML parsing level
    // FeedXMLParseError(xml::parser::Error),
    FeedXMLParseError, // ***TEMP*** until XML crate updated
    /// XML, but feed type not recognized,
    FeedUnknownFeedTypeError,
    /// Required feed field missing or invalid
    FeedFieldError(String),
    /// Got an HTML page instead of an XML page
    FeedWasHTMLError(String)
}
//
//  Encapsulate errors from each of the lower level error types
//
impl error::FromError<hyper::HttpError> for FeedError {
    fn from_error(err: hyper::HttpError) -> FeedError {
        FeedError::FeedHTTPError(err)
    }
}
impl error::FromError<old_io::IoError> for FeedError {
    fn from_error(err: IoError) -> FeedError {
        FeedError::FeedIoError(err)
    }
} 
//impl error::FromError<xml::parser::Error> for FeedError {
//    fn from_error(err: xml::parser::Error) -> FeedError {
//        FeedError::FeedXMLParseError(err)
//    }
//}

impl fmt::Display for FeedError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            &FeedError::FeedIoError(ref xerr) => xerr.fmt(f),   // I/O error
            &FeedError::FeedHTTPError(ref xerr) => xerr.fmt(f), // HTTP error
            // &FeedError::FeedXMLParseError(ref xerr) => xerr.fmt(f), // XML parse error
            &FeedError::FeedXMLParseError => write!(f, "XML parsing error."), // ***TEMP*** until XML crate updated
            &FeedError::FeedUnknownFeedTypeError => write!(f, "Unknown feed type."),
            &FeedError::FeedFieldError(ref s) => write!(f, "Required field \"{}\" missing from RSS/Atom feed.", s),
            &FeedError::FeedWasHTMLError(ref s) => write!(f, "Expected an RSS/ATOM feed but received a web page \"{}\".", s)

        }
    }
}