Rust doesn't have exceptions, but its error handling is less ugly than go. Functions that can encounter errors return a `Result` by convention. If you then want to propagate errors, you can do so nicely with the try! macro, which converts a successful `Result` into its value, or returns the error of an unsuccessful `Result`. Thus the following go and rust are semantically the same:
val, err := error_prone()
if err != nil {
return nil, err
}
let val = try!(error_prone());
Personally, I find that error handling isn't that bad in go. I think it's at least no worse than exceptions. Exceptions make it too easy, for me at least, to just mindlessly propagate the error. Sometimes that's the right thing to do, but in go it's a lot less repulsive to handle the error since to propagate the error you would need handling code anyway.
Rust strikes a nice balance. You have to explicitly handle every error, but if you go with the by far most common handling code, you get some nice sugar that makes it mostly painless.
That's not been my experience, often it helps to add more context to errors as they propagate through the layers. Unless you control the entire stack of function calls top to bottom as part of your application, I think it's ogyrn bad form for one library to bubble up another library's errors directly. It's effectively leaking implementation details and it means the clients of your library may also have to import your dependencies to handle the errors.
The failure mode of a component is a function of how it's implemented. When a library couldn't connect to a server or couldn't open a file, you gain nothing by wrapping that exception, unless it's to indicate the semantic nature of the server or what the file is used for - and these things are normally obvious from the stack trace method names.
Wrapping exceptions as they bubble up is only helpful if the wrapped exception adds context that aids in diagnosing the issue. Otherwise it's just boilerplate that obfuscates and gives a certain type of developer warm fuzzies. "Exposing implementation" is a non-counterargument - the component failed because its implementation preconditions were violated, you need to address those implementation preconditions to fix the problem!
You don't need to import dependencies unless you intend to catch the specific originally thrown exceptions, and that's not normally what happens in the top loop; it normally logs, dumps or transmits the exception message and possibly its stack trace. These operations are normally only rely on the base exception type, so there's no need to catch a more specific exception.
Problem languages for this approach, though, are C++ (where the culture makes a single base class for exceptions awkward) and Java (where throws clauses, a deeply misguided feature, may force dependency imports unless you specify 'throws Exception' - or my preference, wrap in RuntimeException).
PS: I should add the other fundamental problem with the Java throws clause is that it uses static types to analyze dynamic data flow in a language with polymorphism, which makes it a usability disaster. When the code being invoked is indirectly accessed via polymorphic references, exception flow checking falls apart. There's a reason Google Guava's Function and Predicate interfaces don't throw. But that's enough of my bugbears.
> "Exposing implementation" is a non-counterargument - the component failed because its implementation preconditions were violated, you need to address those implementation preconditions to fix the problem!
I don't think this is necessarily true; assuming an XML parsing library that throws exceptions on malformed XML, you are more likely to have simply supplied generally invalid XML than to have hit an implementation-specific exception type.
So, the exception is only implementation-specific in the sense that it was created by an implementation of the class of software you wanted.
But really, in my mind the issue of not wrapping exceptions means all of your client code is now tied to the transitive library, which means the implementation can't be changed without breaking API compat.
Better libraries express their dependencies in a pluggable way, which makes them more testable as well as composable.
The top-level application is usually a composition of libraries.
So in practice, I'm sanguine about apparently transitive dependencies. I prefer when exceptions from one library's plugged-in dependency flow through unmolested, as I see the libraries as mostly a flat dependency of the larger application. If you pull in dependencies transitively without paying much attention to them, you're sowing seeds for trouble with versioning, security scope, risk of stale projects if they're open source, etc.
I think of this as a corollary of Spolsky's law of Leaky Abstractions. Passing through exceptions means not trying to fool anyone into thinking they can work with the high-level abstraction without understanding anything about what's going on underneath.
Ideally the standard library provides base classes that act as core categories for different kinds of exceptions - IO, programmer error, data format error, etc. - so that general code can be written after a fashion when necessary. But I think you're fooling yourself if you think that such general code can be written the majority of the time, much less all the time. What's underneath always leaks through, one way or another, and if you try to ignore the thing that's underneath, you'll end up with brittle inefficient code.
Where high-level wrapping exceptions are justified is where there is an obvious chokepoint in the design - where the interface between several layers is very narrow. But this typically is at the junction between a service and a client - e.g. a web front end and back end, or async job queuing service, etc. - something where there is a service loop and probably service-specific logging of original exceptions, anyway.
> Passing through exceptions means not trying to fool anyone into thinking they can work with the high-level abstraction without understanding anything about what's going on underneath.
Except this is not what happens in reality, very few people see an internal exception type and go investigate the details.
Sure it's shitty practice, but it seems orthogonal to whether the internal exception types are exposed.
I'm not saying people should just ignore what is underneath, I just think that as a library developer, you significantly constrain what you can do without breaking API compat by exposing internal exceptions.
You could take a hard line stance and say that it should be a breaking change and people should go and re-evaluate the library after the change, etc, and then rewrite their code to deal with new exceptions. But in reality it just means that library will no longer get updated, and will effectively become stale for no good reason.
I'm pretty sure that was contributed by Samsung.