Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Checked exceptions are exactly analogous of Result/Either types. They are just built into the language with syntactic sugar, automatically unwrap by default (the most common operation), can be handled on as narrow or wide scope as needed (try-catch blocks), does the correct thing by default (bubbling up), and stores stack traces!

In my book, if anything, they are much much better! Unfortunately they don’t have a flawless implementation, but hopefully languages with first-class effects will change that.



> Checked exceptions are exactly analogous of Result/Either types.

No, they aren't.

They are not compositional. You may want to write `f(g())` but there's no way to write the parameter type of `f` to make this work (in Java). That's because checked exceptions are an "effect" that would require extending the Java type system.


How would you define f in a different language such that f(g()) worked? You couldn’t do that in Go, for instance.


There's at least one language, Koka (https://koka-lang.github.io/koka/doc/book.html) that has effects in its type system. There are probably others. If you don't have effects, you could use union types.


This language looks really cool. Thanks for sharing.


In rust, you write "f(g()?)", and make sure that your function body returns a Result<> with an error type that g()'s error type can be converted to.

It works great. Also, note that f() doesn't care about the type of error returned by g(), and that it will be a compilation error if g()'s error type turns into something incompatible.

Sadly, there are proposals to add exceptions to rust, and it seems likely they will be accepted, breaking error handling semantics across the entire ecosystem (even in new code, since exceptions are too hard to use correctly).


What's the rationale behind adding exceptions?


I'm not sure what they mean by adding exceptions.

AFAIK, the only proposal related to exceptions is adding a `try` block that would scope the `?` operator to that block instead of the current function.


> You couldn’t do that in Go, for instance.

You can – by making f accept g's return types, including the error. This is even being done in the Go standard library: https://pkg.go.dev/text/template#Must


That’s interesting, but consider two functions like func ParseInt(string) (int, error) and func Abs(int) int. You lose some composability when using errors over exceptions. The Rust solution mentioned elsewhere seems elegant.


Simple! You just make f take in g's result type.


... not sure if serious or not



I'm completely serious in the sense that you could do that and really would in some situations.

You might do it because you want to factor the handling of the different result cases out to another function.


You don't think it would limit the independent use of f()?


No? Surely if f() takes a full result (including error condition) it's because there's something it wants to do with that?


You could actually, iirc if f takes as many parameters as g returns it works out.

In a better langage f would take a Result, and then it can manipulate that however it wants.

Obviously you can also plug in adapters if you need some other composition e.g. g().map(f), g().and_then(f), …


Read my last paragraph.


So they are better... in a theoretical implementation.


A prototype of the Java compiler with composable checked exceptions existed at one point, so we know it's definitely doable.


To my mind the big issue with existing examples of checked exceptions is that the language to talk about the exceptions is woefully inadequate, so it stops you from writing things that would be useful while dealing correctly with exceptions. The go-to example is a map function, which we should be able to declare as throwing anything that might be thrown by its argument. Without that we need to either say that map might throw anything and then handle cases that actually can't happen, suppress/collect exceptions inside map, or suppress/collect errors inside the functions we're passing to map, all of which add boilerplate and some of which add imprecision or incorrectness. It would also be good to be able to state that a function handles some exceptions if they are thrown by its argument. And all of this should be able to be composed arbitrarily. And... somehow not be too complicated. For usability, it should probably also be possible to infer what's thrown for functions that are not part of an external API.


No, unfortunately they are not. The problem is not with checked exceptions themselves, but with the other type of exceptions in Java.

In languages that rely on Result/Either for error handling, you've got two types of errors: Typed errors (Result/Either) and untyped panics. Typed errors are supposed to be handled, possibly based on their type, while panics can be recovered from ("catched") but these are serious, unexpected errors and you're not supposed to try to handle them based on their type. Since typed errors generally need to be handled explicitly while untyped errors are unexpected, typed errors are always checked (you can't skip handling them), while untyped errors are unchecked (implicitly propagated up the stack if you don't do anything to catch them).

Java has three types of errors:

1. Checked errors, a.k.a. checked exceptions: (exceptions that inherit from Exception, but not from RuntimeException). 2. Unchecked application errors: exceptions that inherit from RuntimeException. 3. Unchecked fatal errors: exceptions that inherit from Error.

These three kinds of errors live in a confusing class hierarchy, with Throwable covering all of them and unchecked application errors being a special case of checked application errors.

Like everything else designed in the early Java days, it shows an unhealthy obsession with deep class hierarchies (and gratuitous mutability, check out initCause()!). And this is what destroyed the utility of checked exceptions in Java in my opinion.

Consider the following example: We have a purchase() function which can return one of the following errors:

- InsufficientAccountBalance - InvalidPaymentMethod - TransactionBlocked - ServerError - etc.

You want to handle InsufficientAccountBalance by automatically topping up the user's balance if they have auto top-up configured, so you're going to have to catch this error, while letting the rest of the errors propagate up the stack, so an error message could be displayed to the user.

In Rust, you would do something like this:

  account.purchase(request).map_err(|err| match err {
    PurchaseError.InsufficientAccountBalance(available, required) => {
      account.auto_top_up(required - available)?
      account.purchase(request)
    }
    _ => err // Do not handle other error, just let them propagate
  })
In Java, you would generally do the following:

  try {
    account.purchase(request);
  } catch (InsufficientAccountBalance e) {
    account.auto_top_up(e.requiredAmount - e.availableAmount);
    account.purchase(request);
  } catch (Exception e) {
    // We need to catch and wrap all other checked exception types here
    // or the compiler would fail
    throw new WrappedPurchaseException(e);
  }
The "catch (Exception e)" clause doesn't just catch checked exceptions now - it catches every type of exception, and it has to wrap it in another type! Of course, you can also specify every kind of checked exception explicitly, but this is way too tedious and what you get in practice is that most code will just catch a generic Exception (or worse - Throwable!) and wrap that exception or handle it the same way, regardless if it was a NullPointerException caused by a bug in code, an invalid credit card number.

The worst problem of all is that once developers get used to write "catch (Exception e)" everywhere, they start doubting the values of checked exceptions: after all, most of their try clauses seem to have a generic "catch (Exception e)", so does it really matter at all of they're using checked exceptions?

This is the reality. Checked exceptions failed in Java. Most Java developers see them as nothing more than a nuisance and look for ways to bypass them. That does not necessarily mean that the concept of checked exception as a language level facility for errors has failed, but it certainly failed the way it has been implemented in Java.


It's the ergonomics of it. For a checked exception facility to not be maddening, it needs to have good facilities to propagate and wrap exceptions easily and with minimal scaffolding. But for some reason no mainstream language with traditional exception handling did that.

In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.


> In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.

I wholly agree with this sentiment. Rust Result types where also excruciatingly inconvenient to use at the early days, but Rust gradually added facilities to make it better: first the try! macro and if-let, then the ? operator and finally let else. Together with crates like anyhow, thiserror and eyre, error handling became a lot better. I don't use Swift a lot, but it also seem to have iterated on its error handling.

In the 27 years of its existence, Java did very little to improve exception handling facilities. It added exception chaining in Java 1.4 and catching multiple exceptions in Java 7, that's it. I'm not picking up specifically on Java here - I think many languages neglect exceptions or error handling. Go is also an instructive example of a language that chose a non-exception-based error handling mechanism that the designers claimed to be superior, but failed to add ergonomics to that. This is not for the lack of trying though: the Go team tried to fix this issue multiple times, but there are very vocal parts of the Go community who opposed any kind of ergonomics, in favor of "explicitness" (as if explicitness means "error-prone boilerplate"). I would give the Go team full score for seriously trying.

I give them less score on the composition-over-inheritance part though. Go is one of the languages that has objects (structs) and interfaces, but disallows inheritance, but it doesn't provide any mechanism for automating delegation. Kotlin has shown that this is possible and even quite simple. It's not one of these languages features (like method overloading, type classes and generics) that carries a lot of corner cases and complexity that you have to deal with.


If all of those exceptions are custom made, then it makes sense to have a common subclass for them. It is not ideal, I agree, but it is hardly a showstopper.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: