Monday, August 5, 2013

Code infected with exceptions

I believe that code infected with exceptions is bad. Imagine the following harmless code:

public static Foo newFoo(int bar) {
  switch (bar) {
    case 1: 
      return new FooOne();

    case 2: 
      return new FooTwo();

    default: 
      throw new IllegalArgumentException("invalid bar code: " + bar);
  }
}

You use it in the middle of your business code with ease. The code compiles and everybody is happy:

public static List<BazFoo> getAllBazFoo() {
  List<BazFoo> result = new ArrayList<>();
  List<Baz> allBaz = BazDAO.getAllBaz();
  for (Baz baz : allBaz) {
    int id = baz.getId();
    int bar = baz.getBar();
    Foo foo = Foo.newFoo(bar);
    BazFoo e = new BazFoo(id, foo);
    result.add(e);
  }
  return result;
}

The problem is that the call Foo.newFoo(bar) can throw an IllegalArgumentException and when this happens you will have no clue of which Baz was invalid.

To address this and make the message more useful, we have to remember to add a try/catch in the code:

public static List<BazFoo> getAllBazFoo() {
  List<BazFoo> result = new ArrayList<>();
  List<Baz> allBaz = BazDAO.getAllBaz();
  for (Baz baz : allBaz) {
    int id = baz.getId();
    int bar = baz.getBar();
    Foo foo;
    try {
      foo = Foo.newFoo(bar);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("invalid baz: " + id, e);
    }
    BazFoo e = new BazFoo(id, foo);
    result.add(e);
  }
  return result;
}

Add multiple layers of abstraction to your application and you'r ready: spaghetti with an exceptional taste. You add an exception at some point and you will have to review the entire stack of abstractions you have to ensure that the exception does no harm to anyone and has a useful message for future maintenance.

Haskell solves much of the need for exceptions using returns that symbolize failures, e.g. Maybe and Either, and the use of Monad eliminates the need to check the results at each step.

However, there are exceptions in Haskell, why? I can not understand the reason for preferring exceptions rather than special results. In which situations is it better to use exceptions? They look like a modern goto to me.

5 comments:

  1. It is specially hateful when those exceptions are hide in values like "IO (Either Fail Foo)" which seems to be exception free just because its type....but they're not....

    ReplyDelete
  2. A good, old summary on the topic can be found here: http://www.randomhacks.net/articles/2007/03/10/haskell-8-ways-to-report-errors

    The short of it is, in code you write that generates, you should (almost) always be using Maybe and friends. Sometimes, however your code needs to handle information the runtime system sends to you, for example, a user interrupt. This is what the asynchronous exceptions are for; since they are impure, they can only be caught in the IO monad, and technically every operation can throw them, but that would be unenlighteningly tedious to annotate explicitly, so we don't.

    I think the imprecise exceptions are built on the same framework and are a concession to practicality because, while arithmetic operations are technically partial (by virtue of operating on real hardware), it's not useful to have Either around every arithmetic operator's return type.

    ReplyDelete
  3. There will always be exceptions. If someone sends `kill -HUP` to your running process, how do you want that to be represented? Even C has the notion of signals that can be caught. Haskell unifies all these forms of exceptional behavior behind its extensible exceptions framework.

    I also think a distinction needs to be made between optional results (say, looking up a key in a sparse map), and failure results (like looking up a key in a dense map, where you always expect the key to be present). If you make the requirement that errors be detected manually via case analysis and then propagated, you are relying on the discipline of every programmer involved from main down to your function. On the hand, if you use exceptions, it is either caught or it isn't; then if you use resource management (say, ResourceT or bracket) cleanup is implied. If instead we rely on error value propagation, every function involved has to be concerned with proper cleanup, everywhere. That simply gets tedious, which means laziness, which means leakage.

    I think the right solution to this problem is the "failure" package, on Hackage. By returning an instance of Failure, or by working within a monad that supports Failure, you allow the caller to inspect the result as if were a Maybe or an Either -- or, if they simply want to evaluate it directly in IO, it raises an exception. This permits the caller to decide how they want to receive error notifications, rather than enforcing one methodology or the other.

    Lastly, you always have the "try" function at your disposal, if you really want to deal only with error values in your application. After using it for a while, I think you'll find why some of us prefer to use exception handling for IO code. For pure code, I think error values (ideally Failure instances) is the only way to go.

    ReplyDelete
  4. AIUI, there are two main reasons for using exceptions in Haskell: concurrent algorithms and performance.

    Asynchronous exceptions are the only means of implementing certain concurrent communication schemes, or are much simpler than alternatives, so they're widely used in low-level concurrent code (e.g. ghc's RTS). Notably, async exceptions are the only means of interrupting a blocked thread or system call. This is a rather specialized case however, and isn't what most people mean by "using exceptions".

    The other benefit of exceptions is performance. Although `Maybe`, `Either`, and similar types are very nice to use and reason about, they can get in the way of optimizations. At a minimum there's an extra indirection, which can make a big difference in a tight loop.

    Many people would make the argument that exceptions make reasoning about "exceptional circumstances" easier, such as if there's a disk I/O error or similar. While this does make the main/best case simpler, in my opinion handling the exception is usually much more complicated than handling a Maybe/MaybeT construct, so I tend to avoid exceptions when possible.

    ReplyDelete
  5. Because exceptions are for exceptional errors. Maybe and Either are for common errors.

    ReplyDelete