Luke Winikates

25 August 2016

Strict compile-time error checking has a context problem

A favorite quote of mine about the late Jim Weirich was that he was searching for a grand unified theory of software. I love the idea of that goal– programming is complicated, and some of that complexity is irreducible, but I do believe that a lot of the complexity of our profession is self-inflicted. To butcher another quote, programming is pure thought-stuff, and we just don’t understand that material well yet. Is it squishy like gray matter, or rigid like a platonic solid?

I find type systems useful, but also inelegant, and I hope to articulate some of the experiences that have led me to feel that way. The outcome may not be that profound- types are a contrivance that adds some value at the cost of some distraction, while flying without them may force you to keep more state in your head. If we can draw a thick sharpie line around some of the less happy effects of compile-time type checks, maybe that contributes to the grand unified theory, and the design of future programming environments.

My friend and I were working in a Java codebase, dealing with some code that converted a JSON string into a domain object using the Jackson library. The code looked something like this:

Lunchbox lunchbox = new ObjectMapper()
  .read(jsonString, Lunchbox.class);

Some surrounding code ended up being necessary, because the read method is declared with several “checked” exceptions – IOException, JsonParseException, and JsonMappingException as of version 2.5. Checked exceptions are a feature in Java that treats certain exceptions as part of the method signature, checked by the compiler. They’re widely detested today, but putting that aside, the original intent was to promote good coding practices by forcing developers to consider all possible failure modes of their application and address them responsibly.

Research (via @mariofusco) suggests that checked exceptions are mostly ignored, and my personal suspicion is that having to confront 1-3 unlikely edge cases while you’re still trying to draw the bright line through the middle of your implementation is something like having criticism shouted at you while you’re trying to tell a story – it’s just the right thing at the wrong time.

Going back to day of Lunchbox parsing, my friend and I couldn’t think of any reason why we’d ever get these exceptions, because were working in a tightly constrained context. Code elsewhere guaranteed that the jsonString was always going to be valid JSON. We also felt ok going out on a limb that it was always a serialized Lunchbox object, and there wasn’t a high likelihood of that change, and even if there was some chance thereof, our choice of resolution would not have been to catch a json-parsing-specific exception, but to rethink the reasons why we might get into that situation and prevent it. So we wrote code like this:

public Lunchbox lunchboxFromString(String jsonString) {
    try {
        Lunchbox lunchbox = new ObjectMapper.read(jsonString, Lunchbox.class);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

We knew the catch block would never execute – we couldn’t catch and ignore it, because the method signature demands a return value, and making something up or returning null would have been worse. Rethrowing was appealing because in the event that something changed later, the error would still be handled, and would probably escalate up to the web application’s general error handler, returning good old http status 500.

The problem with surfacing errors in the type system of a language is that it means the type system documents error cases that are possible given arbitrary inputs to the method, but may be irrelevant to many contexts, which still bear the tax of “handling” them. In practice, the method may be called in cases where those error cases are not possible, or are so unlikely that they can be safely ignored (compile-type type checking is unfriendly to the idea that not all error cases are actually important to address).

And it’s not even easy to imagine why IOException might be thrown by a method that takes a string and a class object – I suspect that this is because the string form of this method is implemented in terms of a Stream or Reader-based version, and that the string version inherits the IO concern from the more general code it’s built on top of, even though that error case is not possible for strings.

Type systems are generally not expressive enough to capture cases that are possible for some inputs, but not for inputs that actually occur in the current program. This is true not just with checked exceptions, but with languages that use Maybe or Either monads, or algebraic types. More subtle systems like dependent types, or forms of static analysis or annotation that capture constraints or invariants in the local environment (perhaps possible with something like clojure.spec?) could allow for development-time checks to be more valuable.