Hacker News new | ask | show | jobs
by piaste 1040 days ago
> Exceptions do this automatically for you by default (you need to explicitly override it with a try/catch block) but alternative approaches, such as railway oriented programming, require you to add in a whole lot of extra boilerplate code that is easy to forget and easy to get wrong.

The unfortunately missing part of exceptions (in mainstream languages) is that they handle this invisibly. Figuring out, at compile time, what sort of exceptions can appear inside a given function is not obvious.

That's the big payoff of ROP: you can look at any function signature and immediately know what sort of errors can come out of it.

Mitigating the downside of ROP (boilerplate) can be done to various extents, depending on the language. Haskell has do-notation. In F#, using the result computation expression [0] can make your code extremely clean:

    type LoginError = InvalidUser | InvalidPwd | Unauthorized of AuthError

    let login (username : string) (password : string) : Result<AuthToken, LoginError> =
      result {
        // requireSome unwraps a Some value or gives the specified error if None
        let! user = username |> tryGetUser |> Result.requireSome InvalidUser

        // requireTrue gives the specified error if false
        do! user |> isPwdValid password |> Result.requireTrue InvalidPwd

        // Error value is wrapped/transformed (Unauthorized has signature AuthError -> LoginError)
        do! user |> authorize |> Result.mapError Unauthorized

        return user |> createAuthToken
      }
Could we do the reverse, i.e. mitigate the downside of exceptions? Is there a linter, code analyzer, or some other compile-time tool that can integrate with a Java IDE and automatically display the uncaught exceptions that might be thrown by a given line of code?

[0] https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoo...

2 comments

Java has/had a compiler check that forced you to write catch blocks or `throws` annotations in/on functions that call other functions which might throw. The feature is called "checked exceptions" and I believe it has been discarded for its inconvenience by now.

Sometimes it feels like developers are going in circles while trying to find the most optimal way to handle errors.

Checked exceptions are one of the main reasons I’m sticking with Java, even though Java lacks the ability to abstract over sets of checked exceptions, which does cause some inconvenience. It’s unfortunate that no other mainstream languages have been taking that approach.
In languages with a Result type (or Either monad), you basically do have checked exceptions, even though the language has no exceptions.
With result types, you typically don’t get automatic exception propagation. I agree that overall it’s a spectrum of syntactic convenience, checked exceptions effectively form a sum type together with the regular return type.
Java also has that option. There is an excellent library called vavr. It adds the Try and Either monad to Java.
That sounds awesome! Do you have that flag set on a big codebase? Was it a big hassle to turn it on (like you had to remediate a bunch of code that didn't handle exceptions before you could check it in). Have you seen any big changes since enabling it?
Checked exceptions in Java are the default state and much of the standard library uses them.
People are now realizing that having the errors a function can cause right in the type system may actually have been a good idea, but when you point out that Result is not the only way and that Java checked Exceptions do the exact same thing (and so does the Zig error handling mechanism which is a third variant of the idea), they come up with all sorts of easily dismissable nonsense to explain why the two are very different.
What would you reply to this comment:

https://news.ycombinator.com/item?id=37174698

Well, that's kind of true! The fact checked Exceptions are inconvenient doesn't change the fact they are equivalent to returning a Result type (the implementation is obviously different but I think we don't need to mention that).

A future version of Java could totally make it more convenient, and perhaps even make the implementation cheaper such that it would not just nearly the same , but literally the same as in Rust or other similar languages.

Does it even need a new language version? If the compiler already spits out the error "hey, your Fart() function should be annotated with 'throws ButtsException'", couldn't an IDE relatively easily be configured to automatically add the " throws " annotations?
Have you ever used Java?? Any half decent IDE has done that since 2000.
I’m not the parent, but exception declarations are IMO necessary for a stable API contract. It’s exactly the same reason why return types are explicit. The actual issue in Java is that you can’t abstract over an arbitrary-length list (sum) of checked-exception types (variadic type parameters) (with the exception of rethrowing from multi-catch clauses).
After working with railway oriented programming with Arrow in Kotlin I had the same feeling of using checked exceptions.

And please make the Errors generically wrappable in order to avoid losing the traces.

> The unfortunately missing part of exceptions (in mainstream languages) is that they handle this invisibly. Figuring out, at compile time, what sort of exceptions can appear inside a given function is not obvious.

Figuring out, at compile time, what sort of exceptions appear inside a given function is a futile exercise in many contexts, and railway oriented programming does not fix it. Java tried this with checked exceptions and it fell out of favour because it became too unwieldy to manage properly.

In any significantly complex codebase, the number of possible failure modes can be significant, many of them are ones that you do not anticipate, and of those that you can anticipate, many of them are ones that you cannot meaningfully handle there and then on the spot. In these cases, the only thing that you can reasonably do is propagate the error condition up the call stack, performing any cleanup necessary on the way out.

"Handling this invisibly" is also known as "convention over configuration." In languages that use exceptions, everyone understands that this is what is going on and adjusts their assumptions accordingly.

> Java tried this with checked exceptions and it fell out of favour because it became too unwieldy to manage properly.

Because they did a half-assed job of it, and required the user to explicitly propagate error signatures. Inference and exception polymorphism are essential.

Checked exceptions always seemed to me to be an exercise of self-flagellation and enumerating badness; when most of the time there are a handful of specific errors that require special handling, with everything else logged/return error/possibly crash.
The problem is that the callee can’t decide for the caller which exceptions will require special handling. And for the caller to be able to make an informed decision about that, the possible exceptions need to be documented. Since this includes exceptions thrown from further down the call stack, checked exceptions are about the only practical way to ensure that all possible failure modes get documented, so that callers are able to properly take them into account in their program logic.
If you want to (and are able to) document all possible failure modes, then checked exceptions will give you that. As far as I can tell, railway oriented approaches can't.

Unfortunately, you can only do that when the number of possible failure modes is fairly limited. In a complex codebase with lots of different layers, lots of different third party components, and lots of different abstractions and adapters, it can quickly become pretty unwieldy. And then you end up with someone or other deciding to take the easy way out and declaring their method as "throws Exception" which kind of defeats the purpose.

No; you simply abstract the underlying subsystem’s exceptions in your own types, the same way you do with any other type.

And yes, “railway oriented approaches” can absolutely do this.

> No; you simply abstract the underlying subsystem’s exceptions in your own types, the same way you do with any other type.

That's all very well as long as people actually do that. It doesn't always happen in practice. And even when they do, the abstractions are likely to be leaky ones.

> And yes, “railway oriented approaches” can absolutely do this.

How? Please provide a code sample to demonstrate how you would do so.

You adjust the reported failure modes to the abstraction level of the respective function, wrapping underlying exceptions if necessary. You don’t leak implementation details via the exception types. Callers can still unwrap and inspect the underlying original exceptions if they want, but their types won’t typically be part of the function’s interface contract, similar to how specific subtypes of the declared exception types are usually not part of the contract.