top | item 25254515

(no title)

adimitrov | 5 years ago

I really, really dislike exceptions.

Unchecked exceptions don't tell the caller something might go wrong. Fine for Python, where strong guarantees aren't a thing anyway, but any statically typed language cannot be content with essentially adding bottom to every single type.

Checked exceptions have failed, or at least I haven't seen anybody fix their issues. They proliferate spurious exception types in interfaces. They are inflexible, as they usually can't be generic. They suck at typing error cases for higher order functions. They're big heavy and expensive, so can't be used for hot code paths. They're exceptions but more often than not you want to signal expected failure...

The list goes on...

discuss

order

barrkel|5 years 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.

karatinversion|5 years ago

> 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.

stouset|5 years ago

> 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 `?`.

karatinversion|5 years ago

My hot take on this is that java checked exceptions are bad because their design predates java generics. Because of the lack of generics, authors of packages such as java.io had to create god-types of exceptions for their general interfaces to throw. A good example is

  public int java.io.Reader.read() throws IOException
As I see it, the purpose of checked exceptions was to allow declaring expected failure modes in the function signature, so that the programmer (and the compiler!) could check against them - but when my StringReader declares itself capable of throwing an SSLException (subclass of IOException), this benefit is lost. Instead, I must rely on other sources to determine which errors may actually occur, and which I can't do anything about - and the latter I must swallow or pollute all of my package's function signatures with. If the Reader interface had instead been generic

  java.io.Reader<T extends Throwable>
read() could be declared as

  public int read() throws T
This would rescue much, and is something that could be done in modern java; but by the time generics were introduced, all the core packages like java.io were written, and the patterns for how to deal with checked exceptions were set.

joseluisq|5 years ago

Yeah, I dislike also exceptions. I'm more fan of Rust error handling for example via `Result<T, E>`. I believe that make a distinction between recoverable and unrecoverable errors is key.

indiv0|5 years ago

I see a lot of people espousing this viewpoint and I just don't get it. What's the big difference between the way Java does it and the way Rust does it?

In Java, unchecked exceptions are similar to Rust's panics. Sure your less inclined to catch panics than you might be to catching unchecked exceptions, but you can do it in either. Though perhaps Java's Error is closer to a rust panic.

Checked exceptions are just like the Result type IMO. The discoverability of the error surface is the same, you just don't get the nice pattern matching and sum types to handle/represent it.

What I'm getting at is that I'm a fan of the way Rust does error handling, but I don't get _why_ it feels different to the way Java does it. On the surface they're the same, but I loathe the Java approach. Maybe it's just whatever the opposite of rose-tinted glasses is.

MaxBarraclough|5 years ago

You might enjoy this 2005 blog post from the great Raymond Chen titled Cleaner, more elegant, and harder to recognize.

https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...

alquemist|5 years ago

I have to agree with the author. It is extraordinarily difficult to see the difference between bad exception-based code and not-bad exception-based code. In particular, the example of bad vs not-bad doesn't show substantive differences between the two versions.

    // bad
    NotifyIcon CreateNotifyIcon()
    {
      NotifyIcon icon = new NotifyIcon();
      icon.Text = "Blah blah blah";
      icon.Visible = true;
      icon.Icon = new Icon(GetType(), "cool.ico");
      return icon;
    }

    // not-bad
    NotifyIcon CreateNotifyIcon()
    {
      NotifyIcon icon = new NotifyIcon();
      icon.Text = "Blah blah blah";
      icon.Icon = new Icon(GetType(), "cool.ico");
      icon.Visible = true;
      return icon;
    }
What is the problem with the first version and how is the second version fixing it?

renox|5 years ago

Walter Bright (D's author) said something like: who's going to check the error returned by the log functions?

I would add if you want to be correct then the addition signature should be (IntX, IntX) -> Either<IntX,Error>. Are you going to check also the return value of every arithmetic operation?

For me potentials errors are everywhere, so exception aren't bad, even if they make writing 'exception safe' code hard. That's why I was quite interested in the Vale language which claims to improve RAII: https://vale.dev/blog/next-steps-raii

smichel17|5 years ago

What about this middle ground? These are my thoughts, very curious whether there are languages doing this & if so, what the real world problems are.

1. Statically typed.

2. Unchecked exceptions by default. IE, implicit bottom on every type.

3. Optional checked exceptions. Implementation could be `checked` keyword (e.g. `checked fun foo() throws XyzError {}`), or a generic type, or something else depending on the language. In order to mark something as checked, it must catch all exceptions from unchecked values it touches.

4. The compiler infers exceptions types when possible. So, if a regular unchecked function (`fun bar() {}`) only calls checked functions (foo from above), the compiler will carry the information about what can be thrown (XyzError) up the chain, even if there is no explicit signature. This way if another function (`checked fun baz() {}`) calls bar(), then it only has to catch or declare the XyzException, rather than all exceptions generically.

Finally, and to tie this all together: as a matter of style, reserve exceptions for actual unexpected errors. To use an example from downthread, a JSON parsing library should not be throwing parsing errors, it should be returning a Maybe<Parsed, Error>. Why? Because the library cannot determine whether the error is unexpected. If you're using it to parse user input, then yes, you're going to frequently encounter invalid data, and this should be part of your normal control flow, not something that can crash your application at runtime (you may want to log the event, maybe with a stack trace, but without interrupting normal operations). On the other hand, say you're parsing JSON from your database, (also, why are storing json in your db? But bear with me..). And invalid json means data corruption, which means your db is fubar. Then, it's fine to throw, because in that case you're operating on state that you assumed to be impossible, and your program behavior is now undefined.

The key point here is that exceptions are useful as a safety valve for early termination in the case of invalid assumptions. That's a subset of all error handling. If you try to force all error handling into the same paradigm and the same level of reliability, you're going to have awkward edge cases.