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.

3 comments:

JessiTRON said...

Java 8 will save us!

Heath said...

I'm not sure about that. Yes, it will allow cleaner lambdas, but it won't improve generics enough to allow chaining without declaring all the types in the chain:

A method1() throws E { ... }
B method2(A a) throws E { ... }
C method3(B b) throws F { ... }
D method4(C c) throws F { ... }

Idiomatic Java:
try {
D d = method4(method3(method2(method1())));
} catch (E e) { ... } catch (F f) { ... }

Eithers:
Either<A, E> method1() { ... }
Either<B, E> method2(A a) { ... }
Either<C, F> method3(B b) { ... }
Either<D, F> method4(C c) { ... }

JDK8 won't allow me to foldr these methods and preserve type safety because the Eithers don't have matching types.

Unknown said...

Great post! I've been griping about errors lately too. I especially like your point regarding using the type system to help. I've been on a static typing kick lately. Here's my "smart kid" post related to this topic: http://proseand.co.nz/2013/06/05/optiont-null/