Hacker News new | ask | show | jobs
by barrkel 2031 days ago
Spurious exception specifications are the flip side of avoiding not telling the caller something might go wrong. It's a fundamental tension and is unavoidable.

Failure modes are an abstraction violation; they're a function of implementation. That's what makes checked exceptions not work, at the end of the day. Information-carrying exceptions reveal implementation details. So a module author must decide between hiding details and wrapping everything up in module-specific exceptions that user code can't actually use to make decisions most of the time, or expose implementation details that turn into a versioning problem over time.

There's roughly two situations for error handling: near the leaf of the call tree, where you have enough context to deal with an error, and need to switch on error type and take compensating action; or near the root of the call tree, in the main loop, where you log errors and terminate requests etc. in a generic way (e.g. 500 response).

Exceptions aren't ideal for the first situation but are great for the second. Error codes are adequate for the first situation - monadic error types (Result<T>, Either) are a bit better - but suck horribly for the second, because you need to manually unwind, writing boilerplate that should be automatic.

And at the limit, error types are isomorphic to checked exceptions, with the same problems, and more - error types introduce an aggregation problem, where multiple errors need to be joined together. You can still get that with exceptions too but it usually requires parallelism.

2 comments

> There's roughly two situations for error handling: near the leaf of the call tree, where you have enough context to deal with an error, and need to switch on error type and take compensating action; or near the root of the call tree, in the main loop, where you log errors and terminate requests etc. in a generic way (e.g. 500 response).

There is actually a third case: in library code which calls other code which may fail. Take java.io.BufferedReader - to be usable, it has to be at a level of abstraction where it cannot deal with any errors the underlying Reader may throw; but the code using BufferedReader will have provided it with its underlying Reader, and will have a good idea of what errors are reasonable to expect from it.

The reason java's checked exceptions are so bad is that they cannot (or could not, before generics, and hence in most of the standard library do not) serve this use case, leading to checked exceptions that one really can't do anything with.

Sure, and there's also functional composition (functional code has the same problem - what does map(f) return if f throws?).

I think this is covered by the abstraction-breaking nature of failure modes, though. If your BufferedReader exposed the underlying Reader's failure modes, it's not just any BufferedReader any more, it's a BufferedReader<MySpecialReader>, and you don't get runtime polymorphism. You can write more generics to keep the polymorphism in static-land, but then you lose the ability to make choices based on error types.

The incompatibility is between errors and abstraction, not simply a single instance of composition.

> Exceptions aren't ideal for the first situation but are great for the second. Error codes are adequate for the first situation - monadic error types (Result<T>, Either) are a bit better - but suck horribly for the second, because you need to manually unwind, writing boilerplate that should be automatic.

At least in Rust, Result<t> can be unwrapped and bubbled up (in the error case) with a single `?`.