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

In Java functions declares Exceptions in its type signature, so it does all of that automatically. Then you get a compile error if you don't handle it in the function, or you need to declare the function throws it, so it is type safe.

Note that people now consider that as a mistake, people prefer having Exceptions be hidden instead of explicit and requiring handling like that.



We are in the distributed systems age. If systems are composable, operations can fail for reasons that are completely unfathomable to the client. It's not reasonable to have a SharkBitTheOpticFiberCableException and 100,000 other ones that handle every reason why an operation failed.

What the client should know is how an error affects what it is doing, it wants answers to questions like

  * Is it likely this error will recur if I retry immediately? in 1 minute? in 1 day?
  * What is the scope of this error?  Does it affect the entire system?  Does it affect a particular database record?
  * What do I tell the user?
  * What do I tell the system administrator?
Actual improvement in this area won't come from information hiding but it could come out of attaching some kind of ontology to exceptions where exceptions are tagged with information of the above sort, that it is not about having names for them and a hierarchy, but in about having rather arbitrary attributes that help the exception management framework (somewhere high in the call stack!) do the best it can in a bad situation.


While I think this is a good point, a lot of the answers cannot be determined by the generator of the exception. Your SQL library cannot know what the implications of an error are -- is this a minor part of the system for which the error can be logged but mostly ignored, or is it critical? Etc.

People want error handling to vanish so that they can follow the "normal" flow, but in fact error handling is one of the critical things code does.


This is sort of what HTTP codes get at though.

The server doesn't know what you want to do with a 400 but it knows that the problem is with the request for example.


Actually there is somewhat standardized set of SQL error codes and they can be put into a hierarchy like the HTTP codes.

For instance, you can have a SQL error because the syntax of your SQL is wrong. If you're not doing "dynamic SQL" you know this is a programming error (it doesn't matter what input was supplied to the functions.) One common error is "attempted to insert a row with a duplicate key", frequently you want to catch that SQLException and rethrow all the rest.

The ideal SQL library for Java would expose the hierarchy implicit in SQL errors as a class hierarchy.


I had a similar thought a while back [0]. Developing a sane ontology of error types and their implications is a hard problem, but I think it could be done. The subset of errors that is the most frustrating and hard to deal with are ones where, as you point out, the client will have no way to estimate how long a failure mode might persist, at which point you resort to exponential backoff (actually probably an s-curve).

The issue is that sometimes the solution to the issue would require the client to get up and get out a shovel, and go dig somewhere or something. When the abstractions break down that hard there isn't really a way for the developer of the code to handle that unless they somehow stuff a full blown AGI into their program, and even then it would be a stretch.

0. https://github.com/tgbugs/idlib/blob/master/docs/identifiers...


The mistake was not "explicit errors". It was having a mix of error types, some explicit and some implicit, with no convenient way to combine them, plus the interface complications.

Note that most newer languages are choosing explicit errors. This includes at least Go, Rust, Swift, Zig, and Odin.


The second mistake was only flirting with Bertrand Meyer’s work until the Gang of Four showed up and wrecked Java forever.

Meyer + functional core nets you a great deal of code with no exception declarations and an easy path for unit tests. If it hurts to do stuff it might not be the language that sucks, it might be you. Pain is information. Adapt.


Don’t know Meyer’s work - any suggestions for good starting points?


There’s the Design By Contract work of course, but I’m still trying to cite what I thought was his best advice which is to separate decisions from execution, which is compatible with but I find to be subtler than the functional core pattern.

Often we mix glue code and IO code with our business logic, and that makes for tough testing situations. Especially in languages that allow parallel tests. If you fetch data in one function and act upon it in another, you have an easy imperative code structure that provides most of the benefits of Dependency Injection. Your stack traces are also half as deep, and aren’t crowded with objects calling themselves four times in a row before delegating.

    if (this.shouldDoTheThing()) {
       this.doTheThing();
    }
Importantly with this, structure, growth in complexity of the yes/no decision doesn’t increase the complexity of the action code tests, and growth in glue code (auth headers, talking to multiple backends, etc) doesn’t increase the complexity of the logic tests.

A big part of scaling an application is finding ways to make complexity additive or logarithmic, rather than multiplicative. But people miss this because they start off with four tests checking it the wrong way, and it takes four tests to do it the right way. But then later it’s 6 vs 8, and then 8 vs 16, and then it’s straight to the moon after that.


Learn Eiffel. From their own explanation: https://www.eiffel.org/doc/eiffel/Learning_Eiffel

> Remember that Eiffel, unlike other programming languages, is not just a programming language. Instead, it is a full life-cycle framework for software development. As a consequence, learning Eiffel implies learning the Eiffel Method and the Eiffel programming Language. Additionally, the Eiffel development environment EiffelStudio is specifically designed to support the method and language. So having an understanding of the method and language helps you to appreciate the capabilities and behavior of EiffelStudio.

I read "Object-Oriented Software Construction" to do so, but it was long enough ago that I googled "The Eiffel Programming Language" because my brain had substituted that title instead because IMHO, it's more accurate.

The above link should have many current resources for you.


> Note that people now consider that as a mistake

Correction: Some people. Java's checked and unchecked exception approach is quite nice if used judiciously. It certainly beats checking for error after every function call (default: mostly people ignore error codes) and you even get typed errors so you can trivially incorporate exception handling in the conceptual design as a first class design element.

I am frankly not sure how people get confused about "control flow" and exceptions. (In decades of Java programming the only thing that can still cause minor reading/writing nuisance are generic types and type erasure in over elaborate generic code.)


The greatest secret of exceptions is that in most cases you don't need to catch them. What should really be on your fingertips is

   try {
      ... something ... 
   } finally {
      ... clean up ...
   }
this (plus try-with-resources) is the genius of exceptions. The tragedy of exceptions in Java is that checked exceptions convert the above to

   try {
     ... something ...
   } catch(ACheckedExceptionThatHasNothingToDoWithThisCode x) {
      throw new SomeOtherCheckedExceptionToPleaseTheCompiler(x)
   } finally {
      ... do what has to be done ...
   }
with the variations of

   throw new AnUncheckedExceptionSoIDontVandalizeMyCodeMore(x)
and

   catch(...) {
      // i forgot to rethrow the exception but at least the compiler isn't complaining
   }
as well as

   // i forgot to add a finally cause because I was writing meaningless catch clauses
As much as I think checked exceptions are a mistake in Java, it is not hard to make up your mind about rethrows and apply them in a checked or unchecked form with little or no thought.

The unhappy path that you get for free with exceptions is correct for code with ordinary control flow. Most of the code has no global view of the application and is no position to handle errors. On the other hand, for many simple programs, the correct behavior is "abort the program, clean up resources, display an error message" which a sane exception system gives you for free (except for the finally which cleans up the happy path too)

For a complex control flow there is something high up in the call stack that has global responsibility. Imagine a webcrawler which is coordinating multiple threads that call fetchUrl(url) fetchUrl doesn't need to catch exceptions at all, just clean up with finally. What it may need to do is tag exceptions with contextual information that will help the coordinator make decisions. That webcrawler in particular will deal with intermittent failures all the time and only the coordinator is in a position to decide if it wants to retry and on one schedule.


Java isn't generic over exceptions. You can't write a method that takes an instance of Foo and says "my method throws whatever Foo.bar() throws" or even "my method throws iff Foo.bar() throws".

And this means that your method either always demands to be wrapped in a try-catch, or you migrate to unchecked exceptions.

Rust makes errors a part of the regular type system, so they automatically benefit from all its features.


> Java isn't generic over exceptions. You can't write a method that takes an instance of Foo and says "my method throws whatever Foo.bar() throws"

If you bend over far enough backwards, and squint a bit, you can kind of do that...

    interface Runner<E extends Throwable> {
      public void run() throws E;
    }

    class Test {
      public static <E extends Throwable, T extends Runner<E>> void test(T runner) throws E {
        runner.run();
      }
    }
It compiles. I haven't tried running it though.


> You can't write a method that takes an instance of Foo and says "my method throws whatever Foo.bar() throws" or even "my method throws iff Foo.bar() throws".

If you could it'd mean altering the implementation would automatically alter the API, which would be rather unexpected.

That's why the Java approach is to wrap exceptions and propagate causal chains. The underlying errors thrown by the implementation can change but the advertised exceptions don't, but no information is lost.


> Note that people now consider that as a mistake, people prefer having Exceptions be hidden instead of explicit and requiring handling like that.

Well, Swift is a much newer language than Java, and exceptions in Swift cannot be hidden either. And some people do rather like this.


> Note that people now consider that as a mistake, people prefer having Exceptions be hidden instead of explicit and requiring handling like that.

What people? Please tell me where they’re at so I can tell them they are wrong (lol)

But seriously, I could not disagree more.


The designers of the Java functional and stream library for one. None of the functional contracts have throws. So you are forced to have un-checked exceptions for everything, unless you want a truly mind-boggling amount of try-catch everywhere which will rapidly exceed your normal code by factor of 2x-3x.


... or you can just use a sane FP library like

https://github.com/paulhoule/pidove

Some people don't like the Lispy signatures so I did start coding up a version with with a fluent interface but didn't quite finish.

Overall I would say the implementation of lambdas and method references in Java 8 was genius, but the stream library was a big mistake. Part of it is that has this cumbersome API that in principle would let it optimize query execution by looking at the pipeline as a whole but doesn't really take advantage of it.


In Java, yes. Note that the JVM doesn't enforce checked exceptions. It's a language level thing. So in Kotlin for example, where all exceptions are unchecked, you can use the streams library without needing try/catch.




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

Search: