Saturday, June 15, 2013

All the smart kids are writing blog posts about exceptions

Since all the smart kids are writing blog posts about exceptions, I figure if I write one, I might be considered smart by association. :)

I favor automation in all things. I will happily supply metadata with my code that will allow it to be automatically verified to be correct, or at least more correct than it could be without the metadata. This colors my viewpoint on exceptions.

This all started because of Craig Buchek's tweet:
I prefer code that enumerates exactly what it will return and how it might fail via the type system. I used to prefer checked exceptions everywhere for this, but I've since changed to just returning a either a good return value or an error.
Old Java:
class Foo {
  Foo foo() throws FooException {
    if (Math.random() < .5) {
      throw new FooException();
    } else {
      return new Foo();
    }
  }
  Foo bar(Foo foo) throws FooException {
    if (Math.random() < .5) {
      throw new FooException();
    } else {
      return foo.foo();
    }
  }
  String baz() throws BazException {
    try {
      Foo foo = foo();
      Foo barFoo = bar(foo);
      return "I ran the gauntlet with " + barFoo;
    } catch (FooException f) {
      throw new BazException(f);
    }
  }
}

With this code, you can capture all your handling for common errors in one place, and because FooException is a checked exception, the compiler will happily tell you if you forget to handle your error cases. Yay!

However, errors are data just like everything else, and they don't need to be shunted off into a separate world. One might assume that if we remove exceptions, we'll end up with a lot of annoying if checks:
class Foo {
  Either<Foo, FooException> foo() {
    if (Math.random() < .5) {
      return new Either<>(new FooException());
    } else {
      return new Either<>(Foo);
    }
  }
  Either<Foo, FooException> bar(Foo foo) throws FooException {
    if (Math.random() < .5) {
      return new Either<>(new FooException());
    } else {
      return foo.foo();
    }
  }
  Either<String, FooException> baz() throws BazException {
    Either<Foo, FooException> eitherFooOrFooException = foo();
    if (eitherFooOrFooException.left()) {
      Either<Foo, FooException> eitherBarFooOrFooException = bar(eitherFooOrFooException.left());
      if (eitherBarFooOrFooException.left()) {
        return "I ran the gauntlet with " + eitherBarFooOrFooException.left();
      } else {
        return new Either<>(new BazException(eitherBarFooOrFooException.right()));
      }
    } else {
      return new Either<>(new BazException(eitherFooOrFooException.right()));
    }
  }
}

It doesn't have to be that way. Jessica Kerr describes how in detail. However, Java idioms and language limitations do make these functional styles difficult. So, if you're in Java-land, I won't look down upon you for sticking with Exceptions, as long as they're checked.