18 Sept 2012

Error-handling: Java vs Go

FotoBlogr is the first toy I wrote using Go.  As a part of FotoBlogr, I also wrote a Flickr client library, an App Engine helper library, (unpublished) Blogger client, and Picasa client, so I got to see some idiomatic Go.  One thing I really missed when writing Go was exceptions.  Many things that are one-liners in languages with exception support (Java, Python, etc.) become 3 or 4 lines in Go.  Let’s take parsing a string as an integer for example.  In Java, you’d write:
int n = Integer.parseInt(str);
and you’re done.  If you expect this conversion to fail, you’d catch NumberFormatException, but it’s not mandatory.  Compare this to equivalent Go code:
n, err := strconv.Atoi(str)
Of course, you can choose to ignore the error by using an underscore in the place of err, but it still won’t be as expressive like in Java.  For instance, you cannot write return math.Sqrt(strconv.Atoi(str)).  You must write that in two different statements:
n, _ := strconv.Atoi(str)
return math.Sqrt(n)
Of course, this is wrong, and it should actually be something like:
n, err := strconv.Atoi(str)
if err != nil {
  // Handle this error.
}
return math.Sqrt(n)
It was very annoying to get used to writing code this way... almost as if writing dumb code.

Fast forward a few months.  I had moved to a C++ project where the style guide prohibits using exceptions.  Existing code in this project ignored errors in several places, but when I was writing new code I always made sure I did something about errors... at the minimum log it and proceed.  I didn’t have to make a conscious effort to do that; writing Go code had trained me to naturally think about error handling.  It felt awkward to not check for errors, in fact.

Now, I am fixing a bug in a Java project.  This is an old project with design decisions independently made by hundreds of engineer over several years.  The change itself is fairly simple: instead of simply sending a request to an RPC server, now we have to first check whether it’s okay to send that request.  If this query fails, we’d show an error message to the user.  But the complication is this: I’m changing a void method that does not throw any exceptions.  (This method in effect promises all its callers that it can never fail!)  This method is being called by half a dozen classes, which are being called a few others, and so on.  Unless I touch a large number of files to handle the new error, I can’t get the code to compile.

I was discussing this issue with a teammate and what he said was remarkable.  He said, “Every method in every class should throw a checked exception irrespective of whether that method can fail or not.  If it cannot fail today, maybe it can tomorrow.”  He didn’t literally mean that, of course.  What he meant was we weren’t using checked exceptions as extensively as we should have.  If this method I am changing now threw some checked exception, I could simply use that in my client code without scratching my head too much.  This is precisely what Go does.  Almost all functions return an error status.  You can choose to ignore it, but when someone new to the codebase reads it, it’s obvious you’re ignoring an error.  I’m starting to think that Go’s way of error-handling is a much superior way than Java’s and C++’s.

SIDENOTE: The easy and obvious fix is to throw a RuntimeException and catch it in only the places I like to change now.  But that’s a really wrong way to fix a problem like this.  First of all, RuntimeExceptions are not meant for cases like this.  They are for things like division by zero, dereferencing a null pointer, etc.  Things that can happen in any code and thus are impractical to handle specifically.  Since they are rare and usually very serious, they are allowed to propagate through deep call stacks.  Defining application errors, input errors, etc. as RuntimeException can lead to severe debugging issues over time.  You really don’t want that.

2 comments:

  1. if the same method was used in go in many places with _ to ignore the error you would still have to edit all the places when you discover that the new behaviour of the method is to throw exceptions that callee should handle.

    But I like the general idea and I totally agree with how RuntimeException in java gets misused for these kind of cases. In this particular case I think throwing exception from the void method when RPC fails is the right thing to do and will be what eventually need to be done.

    Would be interested in hearing what you did.

    ReplyDelete
    Replies
    1. Correct, but I would only have to change one code path so the error is propagated between my backend function and my servlet. But if the functions in the middle are not designed to propagate the error -- i.e. if they can't return an error -- I'm screwed like I am now. The main difference in Go is that there's no free/automatic error propagation that programmers will come to rely on. Hence many functions will return errors by design, I think. Of course, I could be totally wrong; I have never worked on a similar large Go project.

      What we really did is easy to guess: we made the exception a subclass of RuntimeException :-/

      Delete