Monday, August 25, 2008

Checked exceptions exposes implementation?

I've read and heard the phrase Checked exceptions are evil! They expose implementation details! a couple of time (last time was an entartaining read for several other reasons...). I really don't understand this statement (hey, you, explain to me please).

How can the EncodingException part of the code below expose implementation details when void encode() does not? The first say I can fail to encode, the latter say I can encode. What's the difference?
interface Encoder {
  void encode(Object o) throws EncodingException;
}

For me, checked exceptions are vital to enforce proper error handling. But this can (probably) only be achieved if the exceptions fits the problem domain. For instance, throwing an IOException in the interface above would be really really bad because it exposes details of the Encoder, e.g., that is uses the network, or whatever. On the other hand, the only thing EncodingException exposes is that an Encoder can fail to encode the provided object. That's not an implementation detail, I think, that's the Encoder being honest and not hiding it flaws.

One of the most important lesson I've learnt from using exceptions is the importance of doing try-catch-wrap-throw. For example, if an implementation of Encoder uses a method that throws IOException then the proper way of handling such exception is to catch it, wrap it in a EncodingException and throw it to the client of the Encoder. This cleans up ugly interfaces that throws many exceptions, resulting in clear description (with semantics, yeay!) of what can go wrong when a method is called. Exceptions that fits the problem domain is the key.

A couple of times I've let domain exceptions inherit from each other to define that, for example, a RangeException is a ConfigurationException. This seemed like a good idea to me at the time, however, it seldom helped the design (it didn't make it worse either, though). In fact, the only time I find it useful is when you need to distinguish between thrown exceptions in one place but handle them in the same way in a nother place. For example (where LeftException and RightException inherits from BaseException):

interface Something {
  void doIt() throws LeftException, RightException;
}

class HandlesExceptionsSeparately {
  void method(Something s) {
    try {
      s.doIt()
    } catch(LeftException e) {
      // handle it
    } catch (RightException e) {
      // handle in another way) {
    }
  }
}

class HandlesExceptionsTogether {
  void method(Something s) {
    try {
      s.doIt()
    } catch(BaseException e) {
      // handles both
    }
  }
}

But as I said, this is not very common for me. Although if you are developing a library and wish the user to have the ability to handle the different error-cases separetely, then could be useful.

Well, these are some of my thoughts on checked exceptions. In summary: I think they're good stuff if done well. :)

11 comments:

Ricky Clarkson said...

An alternative to checked exceptions that is more composable is the Either monad.

Replace: int parse(String s) throws Ex

With: Either[int, Reason] parse(String s)

There is also the Maybe (aka Option) monad for when there is no failure reason.

jau said...

You could also use Option[int, Reason]

Ricky Clarkson said...

Option, everywhere I've seen it, has only one type parameter. So Option[int, Reason] would be unusual. The usual name for that is Either.

didroe said...

@Ricky: What do you mean by "more composable"? Exceptions seem pretty composable to me.

Ricky Clarkson said...

didroe,

Consider writing a test that says: for every odd number y between 1 and 100, method x(y) throws an IOException.

for (int a = 1;a < 100;a += 2)
. . try
. . {
. . . . x(a);
. . . . fail();
. . }
. . catch (IOException e)
. . {
. . }

Now consider what happens if x returns Either[Integer, Failure].

for (int a=1; a < 100; a += 2)
. . assertTrue(x(a).isRight);

And with a more convenient syntax (Scala):

1 to 100 by 2 forall (a => x(a).isRight)

The try..catch syntax is statement orientated, i.e., you can't *compose* expressions that do any processing of exceptions.

Togge said...

@Ricky: That test-case would be so much easier to write if Java had closures. In Ruby we have 'assert_raise(SomeException) { piece_of_code }' that tests that a piece of code raises a specified exception.
Actually, closures are not needed per se... it just gives much better syntax.

As I don't know much about Funcitonal Java and haven't tried using Either there are probably cases where Eiter is much better than the closure-approach.

I don't, however, like that error cases are indicated by a return value that you explicitly have to check. But then again, I've never used Either so I'm probably missing somthing in all my ignorance. :)

Ricky Clarkson said...

togge,

You're quite correct, if Java had closures the test case would be easier to write.

Range.from(1, 100).by(2).forall( { int a => raises(SomeException.class){ x(a); } } );

seems to be the best I can get with the BGGA proposal for Java closures, if I invent the Range type with from(int, int), by(int), forall({int => boolean}).

Given your Ruby implementation of this test case, how big a difference is it if you want to adjust the test case to show which elements cause failures? I think it's difficult, because assert_raise itself throws an exception, but I might be wrong.

Either is different to the traditional C-style error code that doesn't get checked, because code that uses Either would typically not otherwise have a void return type. And assuming you read the return value you cannot forget to check it, as your code just won't compile if you do that.

int foo() throws IOException; becomes:

Either[Integer, Failure] foo();, and there's no way to get at the Integer without also 'caring' about the Failure.

Either is more of a concept than necessarily what's in Functional Java, but it's likely that FJ's version is quite sound. I've used FJ once only - most of the features I would use are fun to roll myself. And for work I might need different names, e.g., F becomes Conversion.

Togge said...

It pretty easy to add a message to the assertion:
(1..10).step(2) { |n|
__assert_raise(Exception, "#{n} throws exception") {
____piece_of_code(n)
__}
}

Thank's for the explanation of Either. I understand now that it works differently in functional languages, where the only "side-effect" of a function is its return value. Ignoring a return value is impossible since that the only reason the function is called! :)

Ricky Clarkson said...

I think I need to be clearer:

"Given your Ruby implementation of this test case, how big a difference is it if you want to adjust the test case to show *ALL THE ELEMENTS THAT* cause failures?"

Your version only shows the first, because the assert dumps out of the method at the first problem. In and of itself that's not a bad thing, but because you need to make large (well, large is an overstatement for such small amounts of code) changes for small changes in behaviour, things aren't ideal.

I'm glad you found the explanation of Either interesting.

Togge said...

Ah, yes, that would make the test-case more convoluted and hard to understand. In fact, I'm not even going to try to implement it... I would probably mess it up. :)

I understand what you ment with "more composable" now...

andrei said...

Nice article. I didn't realize that it's possible to expose implementation details this way.