Picture this. You’ve just finished writing a nice, clean function that clearly solves a single problem. It’s beautiful. It’s isolated. You’re happy.
You poke around at it for a while and realize some viable inputs make it explode. Part of you wants good error handling to keep the end user sane, but another part of you instinctively doesn’t want to wrap that beautiful code in another form. The code is pure as-is. With a mildly nauseous feeling in your stomach, you catch the exceptions and handle them inside your function. At least your end user won’t hate you, I guess.
I’ve had the above experience too many times. And until recently, I didn’t know what to do about it. I believe this is in part why Clojure’s infamous for poor error messages. It’s a pain to pollute code that solves your problem with error handlers. It’s worth taking the time to explain what bugs me so much about try/catch, not just in Clojure, but in any language that takes the same approach to error handling:
- It’s a clear violation of the Single Responsibility Principle. By definition, anything function using a try/catch is doing at least two things: application logic and handling an error for one exception type. It only gets worse as more exception types are propagated up the call chain. And let’s not forget the finally clause! So that’s 1 responsibility for application logic, n responsibilities for n catch clauses, and 1 responsibility for the finally clause to do any clean up. This does way too much.
- It’s a violation of the Open/Closed Principle. If you want to add another catch clause, you have to dive into the guts of a function. That bothers me a lot.
- It’s order-complected. There’s incidental complexity in the sequence that you list which exception types you’re going to handle before others. Rich scorns ordering when it’s not necessary, which is exemplified by his views on pattern matching and long function parameter lists (Simple Made Easy).
Now that I’ve spelled out my problem with traditional error handling in Clojure, we can move onto the solution. In Rich’s talk The Language of the System, he directs the audience to read Joe Armstrong’s (the creator of Erlang) dissertation on error handling. Rich said something to the effect that it takes error handling and completely turns it on its head. I was curious, so I gave it a read. If you haven’t read it before, check it out. It’s a quick read filled with a lot wisdom.
In his paper, he lays out 4 main points that Erlang’s error handling was built around to provide incredible fault-tolerancy (page 106):
- Clean separate of issues
- Special processes for error handling
- Capability for application logic and error handling code to run on different physical machines
- Ease of creating general error handlers
Points 2 and 3 are very specific to Erlang, so I won’t touch on those in this post. Read the paper to get a better idea of his philosophy. Points 1 and 4, however, made great candidates for the kind of functionality I wanted to bring to Clojure.
The goals for the functionality I wanted to introduce can be enumerated by a few points:
- Total separation of application logic from error handling logic. One responsibility per logical unit of code.
- Respect the Open/Closed Principle and make it a “no-think-operation” to add more error handlers. In short, make it feel like multimethods.
- Avoid order-complected sequences of error handling.
These goals match up as solutions to the problems I listed about try/catch earlier. Cool, sounds like I’m on the right track.
So I sat down and wrote the darn thing. I called it Dire (GitHub source) because I felt that we could use it that badly. Let’s see some code:
Compare that with Dire’s approach:
There’s a concise API for adding catch clauses, finally clauses, and even pre and post conditions. There’s also non-bang (!) variants that don’t mutate your functions (they’re the second bunch of examples listed on GitHub).
Speaking of pre and post conditions. You know what the problem is with assertions of that nature in virtually all languages? They only offer error detection! Error handling is a two part mechanism. Part 1 is detection: check. What we’re missing is part two: Reaction. When things have been detected to have gone awry, what are we going to do about it? Dire let’s you react with arbitrary defined functions. I think turning off assertions when we’re not in development mode is a bad idea. We’re throwing away the chance to gracefully conduct our program when it’s in danger.
So please, let me know what you think. Was this a worthwhile effort? Tweet me at @MichaelDrogalis