← Back to context

Comment by spullara

2 days ago

always has amazed me that there was so much hate for java checked exceptions where handling errors ought to be the norm.

In the vast majority of programs, errors and non-results are irrelevant at the call site. You want to program for the happy path and not wrap every single call with error checking code or `try/catch/finally`.

    h = file_open(path)
    data = file_read(h)
    file_close(h)
    return data

is the code you want to read. This essentially is:

    return file_open(path).success(h -> h, file_read(h)).success(h, data -> file_close(h); data)

A language with thoughtful error handling primitives will support such code.

  • > You want to program for the happy path

    This is the root of a lot of the evil. Yes, humans want to program for the happy path. It is a lot more fun. However, that bias leads to so much lack of diligence to properly deal with the unhappy path.

    • Syntax is not going to solve logic errors. No amount of checked exceptions or forced unwrapping of a result will help with TOCTOU or any number of similar vulnerabilities in code.

      You ideally want to attack the problem at two ends:

      - make the syntax noise-free so you can see what is happening

      - build primitives at the syscall and stdlib level for common footguns

      You would still need to deal with domain-specific logic errors, but you have at least reduced the surface area exposed to casual attacks.

    • > This is the root of a lot of the evil

      It is not. Being extremely restrictive in the happy path, throwing all over the place, but have generic error handling code at the top level (e.g. HTTP entry point) makes for some pretty robust, stable, easy to read software. Albeit it typically has somewhat rough user experience.

      The thing is, that's a pretty good place to start. From there on, you can handle exceptions one by one, surgically, and make things nicer.

      1 reply →

    • People using exceptions want to handle the unhappy path, just usually not at the call site.

The question is _where_ the error needs to be handled. It's almost never directly at the call site. And then there's a few issues with checked exceptions and Java language things (RMI, closures, etc) which make checked exceptions cumbersome. An exception (checked or not) might be worse than sum types, but they're still a thousand times better then returning an error in-band.

  • > The question is _where_ the error needs to be handled. It's almost never directly at the call site.

    Which is why exceptions in Java can bubble up the stack? You don't have to catch it in a particular method you can let it go to the caller of the method, and so on.

    • Checked exceptions must be handled or declared. You end up with everything saying it can throw several random things like IOException.

  • Rust and Go literally have you handle it at the call site by default and if you don't want to you have to pass it on.

    • Go doesn't require you to handle or pass an error upwards. You can call something that returns err and just ignore it, compiler won't care. The "if err != nil" thing is annoying too. Rust made the right compromise with !

    • Which (at least the way it was done in Go) is ignoring 20 years of experience in programming language design.

Better design breeds better habits. In Java, you can end up handling a DNS exception 20 layers up the stack, even if your call never actually triggers a DNS lookup, simply because some method deep in the call stack declares it as possible. The culture is to propagate or wrap exceptions rather than translate them into domain-specific errors near the source.

Rust makes handling and mapping `Result` locally more ergonomic, even though `?` makes propagation easy too. Rust, however, also makes it easy to lose the context during propagation and end up with useless `Error`.

I agree but I think was also because Java didn't have sum types (or still doesn't? years I haven't read Java) and rethrowing is super easy, while Rust kinda nudges you into the right direction (if you need to rethrow at all, think if you want to rethrow verbatim or wrap with a semantic error type).

Right idea at the wrong time.

  • yep, 90% ergonomics in my view. If it had its time again with modern ergonomics thing could look very different.

They were overused, but some actually make sense, such as the IOException and SQLException

  • The problem with them all always comes when the ever-steady march of abstractions, features, and edge cases results in one routine throwing the same two error classes but in different places, and how you handle them is different despite being the same wrapper. Add a thousand developers and several years of churn, and poof you get extremely complex bugs that result from errors seeming to come from one place, but actually coming from another.

    Having been on the receiving end of debugging that a few times, I no longer mind the in-band error handling, although admittedly I'm inexperienced enough to be confident its better. Just for the random code I've had to read where I just need to know exactly how something I didn't write and will never read again works, and whether the error is handled and how, i love reading in-band error handling, even if I too dislike the way an error laden routine must be written. Over time I've come to appreciate elegance "in the large" as opposed to the code I see in any individual function.

I always thought the ideas was great, but type system was holding it back. If you could have expressed that your thrown exceptions are the same as some other method's (e.g. a method of a class given as a parameter), and some other classes, then it could have been a lot less annoying. Powerful, even! But Java did take its sweet time to get even type parameters.

I have written very little Java, though, so this is more of an armchair analyst position.

Amen, people saw the symptoms of "needs more syntactic sugar" and misinterpreted it into "rigorous error handling is bad".

The same way that layered code should react/wrap/translate when it comes to return types, it should do the same for thrown exceptions, and checked-exceptions helps you do that. They're both results of computation.