Hacker News new | past | comments | ask | show | jobs | submit login

In my experience, APIs that throw rarely define all the exceptions that can come from it, especially transitively. I see exceptions as a failed (because undocumented, but still important for correctness) attempt at compromising between halting and returning an error.



You can still, at the very least, `catch(Exception e)` or `catch(...)` to handle a failure. Odds are very good in my experience that you will anyway have nothing better to do than log the error and abort the higher-level operation (e.g. HTTP request handler), even if you do know the specific type of exception that happened.

Also, even in languages that have error return types/codes, it's very uncommon to see anything other than the most generic error value/return code allowed by the convention (e.g. `return -1` in C or `return fmt.Errorf("...")` in Go). Writing an API to document all possible failure modes is hard, and is rarely done, regardless of the mechanics of how errors are returned.

One of the exceptions I've often seen is in SQL APIs, which generally do need to report the specific SQL errors that were signaled. And here, I've seen all possible errors explicitly exposed in the API, typically through a rich Error or Exception type that has a field for the specific SQL error code.

Not to mention, we were mostly discussing cases where a library finds itself in a bug situation, say a null pointer case. I would not expect the API to express the possibility of "NullPointerException" or "ArrayIndexOutOfBounds" as possible return values, but I do want the language to raise these and allow me to decide how to handle them instead of simply halting the entire program - at least in managed memory languages where memory corruption is not possible/likely (if there is a good chance of memory corruption, like in C++, halting is indeed much better than raising an exception).


Why would you ever want to handle a null pointer or index out-of-bounds error from a code that's not yours (i.e. basically a black box)?


I wonder if there's a moral equivalent of borrow semantics where we more formally define error propagation.


Java does this with checked exceptions and it’s a huge hassle to work with.


To be fair, there are two reasons why it's considered a hassle.

One reason, which is a bad reason, is that many people just don't like to document and handle errors. Instead of being happy that the compiler is telling them that they forgot to handle or declare an IOException from this call, they get annoyed that it's yelling at them "for no reason". This is simply lack of understanding/care for how you program.

The other reason, which is actually a problem with the language that could be fixed, is that Java doesn't allow functions to be polymorphic in their Exception types, like it does for argument and return types. This makes higher-order constructs very annoying - for example, `stream.map(Function)` should `throw` the same Exceptions that `function` throws, as should `Arrays.sort(array, Comparator)`. Without this capability, you end up with an extremely ugly and brittle pattern of doing:

  //V foo(T x) throws SomeException();
  
  Stream<V> bar(Stream<T> stream) throws SomeException {
    try {
      return stream.map( (x) -> {
        try {
          return foo(x);
        } catch (SomeException e) {
          throw new RuntimeException(e);
        });
    } catch (RuntimeException e) {
      if (e.getCause() instanceof SomeException) {
        throw (SomeException)e.cause();
      } else {
        throw e;
      }
    }
  }
When all you wanted was:

  Stream<V> bar(Stream<T> stream) throws SomeException {
      return stream.map(Foos::foo);
  }
This huge verbosity is obviously unnecessary and ugly, and could be removed with some compiler support (compiler could just insert this), or some more complex runtime support. In my opinion, if Java did this, it would actually have the best error handling mechanism of any language on the market - much better than Haskell or Rust.


> One reason, which is a bad reason, is that many people just don't like to document and handle errors. Instead of being happy that the compiler is telling them that they forgot to handle or declare an IOException from this call, they get annoyed that it's yelling at them "for no reason". This is simply lack of understanding/care for how you program.

Usually, what happens is that you call some library code (or some code that a teammate wrote) and that code will declare an IOException or something like that. In many cases, there's no point in handling that, as that file that you're trying to open or similar isn't supplied by the user but e.g. a static resource. That's the entire point of the article, that panicking when encountering a violated precondition is totally acceptable.

The unchecked vs. checked exception distinction also suffers from the fact that it's usually the call site, and not the declaration site, that knows whether an exception is recoverable or not.

Java checked exceptions would be fine if it had something like "unwrap", which just converts a checked exception to an unchecked one, but it doesn't, and that's why everyone kind of hates them.


> Java checked exceptions would be fine if it had something like "unwrap", which just converts a checked exception to an unchecked one, but it doesn't, and that's why everyone kind of hates them.

It's a bit verbose, but try{ doThing();} catch (Exception e) {throw new RuntimeException(e) ;} is exactly that, and in today's Java it is very easy to put in a utility function.

> The unchecked vs. checked exception distinction also suffers from the fact that it's usually the call site, and not the declaration site, that knows whether an exception is recoverable or not.

But that is true of every possible compiler-enforced error handling mechanism. The declaration site is responsible for declaring what errors it can produce (through exceptions, result types, multiple returns, error codes, the Either monad etc.), and the compiler ensures that every call site handles those error values in some way.


> It's a bit verbose, but try{ doThing();} catch (Exception e) {throw new RuntimeException(e) ;} is exactly that [...]

The verbosity is exactly what is bothering most people. It's annoying to write and clutters the code. Even if you yourself agree that it should be written like that, your team mates will maybe not. The "swallow an error and pretend it didn't happen" pattern is incredibly common in Java from my experience and it simply wouldn't be so common if there existed something like "unwrap()".

> and in today's Java it is very easy to put in a utility function.

I had to do quite a bit of trial and error and googling to see how this can be done (you have to use the "Callable" interface, instead of something like "Supplier"). I suspect, this will be the same for most people, who wouldn't know how to write this, or just wouldn't bother.

And even if you add this, you'd have to call it from some utility class, which makes the call site much more verbose.

Defaults matter, and if there is no good built-in solution for this in Java, people will not use it.

> But that is true of every possible compiler-enforced error handling mechanism.

Not every language bothers with a compiler-enforced handling mechanism, precisely because it's hard for the compiler to predict whether an error should be recovered from or not, or at which part of the stack it should be handled.

If you do want compiler-enforced error handling, result types are probably better because they are regular language constructs that can be manipulated and passed around in regular ways, and it's easy to convert them to a panic, as with Rust's "unwrap()".

This is also why Kotlin switched away from checked exceptions and now maintains that if you do care about compiler-enforced error handling, you should use Result types instead.

Now, that said, I wouldn't mind better tooling in languages with exceptions (Java and Kotlin, for example), where I could ask the compiler about a function and it could tell me about all exceptions that could (transitively) be thrown from that function, or an annotation to the effect of "please, compiler, verify that this function only throws these types of exceptions (be they checked or unchecked)". But that's something to be used judiciously for some critical code paths, and not everywhere necessarily.


> The verbosity is exactly what is bothering most people. It's annoying to write and clutters the code. Even if you yourself agree that it should be written like that, your team mates will maybe not.

As I said, a bad reason to dislike compiler-enforced error handling. Especially since we're now discussing the small minority of places where you want to call a function that returns errors, but you believe those errors are not possible in your case.

> I had to do quite a bit of trial and error and googling to see how this can be done

I admit that only after writing that I remembered that Java has a dozen different interfaces that represent various flavors of functions, so indeed it's not that easy to write the utility I was thinking of.

> If you do want compiler-enforced error handling, result types are probably better because they are regular language constructs that can be manipulated and passed around in regular ways, and it's easy to convert them to a panic, as with Rust's "unwrap()"

I never understand this point, though I've seen it raised a lot in these types of discussions. Exceptions are also regular language constructs, there is nothing that magical about them. All of the problems that people list with checked exceptions are there for Result types as well, and then some. You try writing the result type for a function that can fail in 20 different ways, or use that result type to handle 2 specific error types and ignore the 18 others.

Note that another name for a "panic()" is "throwing an unchecked exception".

> This is also why Kotlin switched away from checked exceptions and now maintains that if you do care about compiler-enforced error handling, you should use Result types instead.

I have looked at their docs, and they do no such thing. They took the same path as C#, and never added checked exceptions in the first place, and cite C#'s designers for this decision in their docs, and a maintainer for what later became apache-commons. They give various reasons, that all apply to result types just as much - accumulation of error types, functions/interfaces that want to conditionally throw exceptions, interface breaking when a new error type is added to what a function can return etc.

All of these can be handled to various degrees. Unfortunately Java, while somewhat improved, is still an exceptionally verbose language, and this shows in its exception handling as well.


> As I said, a bad reason to dislike compiler-enforced error handling. Especially since we're now discussing the small minority of places where you want to call a function that returns errors, but you believe those errors are not possible in your case.

This comment thread is in response to an article in which it is (IMHO correctly) argued that "just let it crash/panic" is the correct response in many cases, so it's not a "minority of situations", in my view.

> I never understand this point, though I've seen it raised a lot in these types of discussions. Exceptions are also regular language constructs, there is nothing that magical about them.

You showed yourself how checked exceptions in Java mess up higher-order functions. That's because you can't deal generically with functions that may or may not throw exceptions, since return values and exceptions are different language mechanisms. Sure, Java could add special handling for map etc., but barring that, how would you actually implement "map" yourself? You would need to be able to parameterise the mapping function according to whether it throws an exception, and if so, which ones (it could be multiple ones!). To my knowledge, there is simply no language construct in Java that allows you to even express something like that. Result types allow you to simply handle that in library code, where a function can just pass on the return value of another function.

Moreover, you can write code that is generic in terms of whether it's dealing with an optional, a result type or a list (for which "empty list" can signal "no results", which is in a sense similar in kind to optional or result). That's because all these types have a monad instance. That means that you can convert code that returns "null" into code that returns result types (if you want to add more information to the error condition), without the caller needing to be aware of it. That's simply not possible with exceptions.

> Note that another name for a "panic()" is "throwing an unchecked exception".

And I don't generally mind unchecked exceptions, I mind checkef exceptions. Panics are a bit more limited in scope than your regular Java unchecked exceptions, though (not in small part due that a language like Rust just has a better type system that allows you to to express more invariants - at least historically, I know that Java has sealed interfaces now too), and there are restrictions in terms of how you can recover from them - which should dissuade people from abusing them for control flow, something that is common in Java.

> I have looked at their docs, and they do no such thing.

They probably don't think that code guidelines belong in official documentation, but:

https://elizarov.medium.com/kotlin-and-exceptions-8062f589d0...

(Roman Elizarov is the project lead for the Kotlin language)


One of the early lambda proposals for Java - the one by Neal Gafter, if I remember correctly - actually had exception parameters for generics (including type unions), so you could do HOFs like that. But, at the end of the day, they went for something simpler.


It doesn’t have to be. Nim’s checked exception-like “raises” pragma is quite lovely to work with.




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

Search: