Java – Why do “checked exceptions”, i.e., “value-or-error return values”, work well in Rust and Go but not in Java

exceptionsgojavalanguage-designrust

Java has "checked exceptions", which force the caller of the method to either handle an exception or to rethrow it, e.g.

// requires ParseException to be handled or rethrown
int i = NumberFormat.getIntegerInstance().parse("42").intValue();

Other, more recent languages such as Go and Rust use multiple return values instead:

i, err := strconv.Atoi("42")    // Go

match "42".parse::<i32>() {     // Rust
  Ok(n) => do_something_with(n),
  Err(e) => ...,
}

The underlying concept is similar: The caller of the method has to do something about potential errors and can't just let them "bubble up the stack trace" by default (as would be the case with non-checked exceptions). From some points of view, checked exceptions can be seen as syntactic sugar for alternative return values.

However, checked exceptions are widely disliked. The C# designers made the deliberate decision to not have them. On the other hand, Go and Rust and extremely popular.


Why did this concept (see the bolded sentence above) fail in Java but succeed in Go and Rust? What mistakes did the Java designers make that the Go and Rust designers didn't? And what can we learn about programming language design from that?

Best Answer

From a scientific point of view, checked exceptions can be seen as alternative return values, e.g.

Exactly. They can be seen that way, and they should be but they aren't.

Using an Error type like is common in Rust, Elm, Haskell, and in some sub-communities in Scala or a special error value as in Go is just an alternative return value indicated in the type system. A checked exception is like an alternative return value, but it doesn't use the normal way of returning values, it is a completely separate, very different way of "returning values". It also sits outside of the type system, and bolts on a completely separate "checked exception" system onto the type system.

But most importantly, it is not just an alternative return value, it is also an alternative control flow.

Another problem with the specific way checked exceptions are implemented in Java, is that they are anti-modular. That is, however, not a fundamental problem of checked exceptions, unlike the ones I mentioned above. There is an idea of Modular Anchored Exceptions, for example, where you can specify something like

int foo() throws like bar { return bar(); }

And you don't have to know (and leak!) which precise exceptions bar can throw. You can even do something like throws like bar except ArrayOutOfBoundsException when you are handling some errors yourself.