Now, Rust macros? Those are gross. The module system is love-it-or-hate-it. The syntax could be most charitably described as "classical" and the compiler itself is held together with reams of duct tape. But it's the first language that's attempting to explore this design space in an industrial setting and encounter all the problems therein (for god's sake, they had to reinvent the notion of closures (and even if you superficially believe they're similar to C++11 closures, the mechanisms required to make C++-style closures memory-safe are novel AFAICT)). Despite all of its flaws (and the upcoming 1.0-stable release will have flaws), it's a remarkably worthwhile language to learn and will influence every language to come that cares to call itself a systems programming language, all because the core ownership system has been so long iterated-upon (and even that will continue to see ergonomic improvements as time goes on).
It took several go-rounds to make Python exceptions generally useful. Originally, exceptions were just strings. Exceptions became more useful when an standard hierarchy of exceptions was defined and libraries were modified to use it. The Python exception hierarchy has a root of Exception, and one of its subclasses is EnvironmentError. Errors that reflect problems external to the program, such as I/O, network, and external data format errors, are subclasses of EnvironmentError. So if you catch EnvironmentError and report it properly, that covers most common cases. Exceptions not under EnvironmentError generally indicate program bugs, so those are usually allowed to propagate upward to the point where crash-level errors are handled. The default is a program abort and a stack backtrace.
The other problem is getting things closed out properly after an exception. C++ tried doing this by having destructors close things. This works until something goes wrong in a destructor. Exceptions raised within destructors tend to cause problems. This tends to result in ignoring errors at close time. Go's "defer" has the same problem. Calling destructors from a garbage collector is even worse. Microsoft tried to address this in Microsoft Managed C++, resulting in painfully complicated destructor semantics. (Look up "re-animation" for Managed C++, if you like.)
The Python solution was a "with" clause. This is borrowed from Common LISP's "(with-open-file ..) construct. The Python syntax is "with opening expression as object handle :". The opened object is open for the scope of the with clause, and upon exit from the with clause, the "__exit__" method of the object will be called, no matter how the with clause is executed. To make this work, I/O, locks, and database connectors had to be retrofitted with "__exit__" methods. This was done around Python 2.5-2.6; it took a while.
The result is that Python functions that open and close things tend to have this pattern:
def dosomething(args) :
try:
with *thingtoopen* as *handle1* :
with *otherthingtoopen* as *handle2* :
*do work on open things*
except EnvironmentError as message :
*report errors which occurred opening things, working on them, or closing them*
This is reasonably simple, and tends to result in programs that do something reasonable for error conditions. Importantly, even if an exception is raised, the close operations take place. Even the case of a close operation raising an exception is properly handled; the other close operations still take place, but they are aware there's an exception situation. A database connector would do a ROLLBACK instead of a COMMIT in such a situation.This is probably the direction Rust should have taken. Error handling in Rust requires excessive boilerplate code every place an error can occur. The new Rust error hierarchy scheme [1] is especially verbose.
The problems with exceptions are well-documented (they require the programmer to write all his or her code transactionally, leading to so-called exception safety issues). Subclassing doesn't address the major issues of error handling at all, as far as I know, but again--I'd be interested to hear how you feel they are different (Rust already has an IOError that suffices for the cases you seem to be concerned about, but in practice there are many more types of error than that).
//
// 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)] // crank out default functions
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::BuilderError),
/// 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::BuilderError> for FeedError {
fn from_error(err: xml::BuilderError) -> 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::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)
//_(ref xerr) => xerr.fmt(f) // would be convenient, but not allowed in Rust.
}
}
}
[1] http://lucumr.pocoo.org/2014/11/6/error-handling-in-rust/I'm frankly not sure why you think this is boilerplate, though: what, indeed, is wrong with this picture? Maybe I'm simply missing the obvious, but this doesn't even seem like that much code to me, especially given that each of your error types would have to be classes in other languages. (BTW, you don't need to prefix all the errors with Feed because they are already namespaced). Once you've written the above, you can simply try! everywhere and get automatic conversions--and you use try! a lot more often than you define new result types.
Your example also looks very much like what I would write in a language like Java, where exceptions are actually part of the signature (which is the approach Rust takes). Perhaps you are talking to the wrong person here, but I have never had an issue with Java's approach to checked exceptions, only the many ways the language provides to undermine them and the difficulty of creating new error types without ADTs.
In any case, you still haven't addressed my remaining questions re: exceptions. We may disagree on whether the above is boilerplate, but given that exceptions have other problems they are still not a drop-in replacement. I think if you consider boilerplate to be the biggest issue in error handling, you and I likely fundamentally disagree.