Idiomatic errors in Clojure
Throw an Exception
It’s idiomatic Clojure to handle an error by throwing a native exception. After all, Clojure is hosted by nature.
(when-not (blah blah)
(throw (IllegalArgumentException. "foo can’t baz")))
or
(throw (js/Error. "Oops!"))
Let’s not overcomplicate this. Interop is idiomatic.
Throw ex-info
Except…native exception classes aren’t always a joy to work with. Selecting the right one is a hassle. They complicate cross-platform code. Worst of all, they can’t carry data! So throw an ex-info
instead, and shove whatever keyvals you need in it.
(throw (ex-info "Foo is borked" (select-keys foo […])))
Use the data-conveying tools native to Clojure and move on.
Return an error map
Sometimes throwing an exception doesn’t feel right. Maybe it feels too much like a GOTO
, maybe you want to explicitly keep the control flow local. So return an error map, like:
{:ok false :foo …}
or
{:failure :fail.baz/missing-foo, :bar :baz, …}
Lightweight tricks like this are idiomatic in a dynamic lisp.
You’ll want some utilities on hand to take advantage of this approach. For instance, imagine some->
threading, but bailing from the happy path not on nil
but when a map contains a :failure
key. To handle the result, you’ll want an if-let
analogue. These aren’t difficult for an intermediate Clojurian to write from scratch, and preferences vary on implementation details like naming. A lightweight version fits in a gist; for a more involved approach see thread-until.
When is this useful? I preferred it when building a pipeline to process a large number of items at a time. It didn’t make sense, if #22,051 in a sequence of 30,000 items failed, to pull the emergency brake and stop the whole train. An error map felt right because, being data, it could be passed around until I was ready to handle it. It was a small, convenient step up from returning nil
.
Sean Corfield described how he uses this technique:
- if something is unexpected and the immediate code cannot handle it, throw an exception
- if something is expected then return either
nil
or some{:ok false :message "..."}
value (and{:ok true :value ...}
for success)
We can do this because Clojure isn’t bound by Java’s type system straitjacket. As Doug Crockford pointed out:
The Java language encourages misuse of exceptions as a way to get around problems in its type system. A Java method can only return a single type of result, so exceptions are used as an alternate channel to return the ordinary results that the type system does not allow.
(A similar point is made in this archived Clojure design document.)
Structure your ex-info
s
Maybe those anything-goes ex-info
and error maps make you feel groundless. Those who crave an ontological bedrock are free to impose order with something like Cognitect’s anomalies lib. It categorizes failures with an ::anom/category
key holding a value like ::anom/not-found
. Its implementation is like a ghost — a great example of how little engineering it takes to make your own.
Self-imposed standardization is a good idea when using a dynamic language, and quite idiomatic.
Flowing errors
Some developers crave something like error maps but more concrete. Particulars vary but the goal is usually to rearrange control flow to show the happy path while smoothly handling errors.
We have Ivan Grishaev’s pact, fmnoise’s monad-rejecting flow, Adam Bard’s monad-embracing failjure, Druids’ railway-oriented (and secretly-monadic) rop, lazy-cat’s pair-centric tenet, Shantanu Kumar’s casually-monadic promenade…there is no shortage of implementations.
I find it helps to visualize these libs populating a spectrum of “Either Monad-ness”. Boring old error maps (or even nil
) would be on one far end.
Maybe you’re drawn to one of these, maybe they don’t make sense for what you’re working on. Regardless, I’m glad Clojure makes it easy to grow your own custom error handling, and that the community shares so many of theirs. DIY is idiomatic.
Ceci n'est pas une Error
There’s a footgun to avoid when catching and throwing errors in JVM Clojure.
Java distinguishes between two Throwable kinds: Exceptions which are intended to be caught, and Errors, which indicate ”serious problems that a reasonable application should not try to catch“. Few recommendations are disrespected more often in Clojureland. The Clojure idiom here is muddy.
It’s common to see (catch Throwable)
sprinkled liberally across a Clojure codebase. Doing so is generally harmless but occasionally makes failures unnecessarily more difficult to debug and recover from (for example).
Catching Throwable
is hard to untangle from casual misuse of AssertionError, which is thrown by assert
and :pre
/:post
conditions. Many Clojurians use these tools as if they threw IllegalArgumentException
. Just as with catching Throwable
, this doesn’t often present an immediate problem. Trouble arises in the mismatch between its intended use case, “an informal design-by-contract style of programming”, and the off-label use so often seen in Clojure.
Alex Miller summarized the issue on Clojurians Slack:
Assertions are designed so that they can be turned off in production to remove the cost of the assertion. They are designed to catch logically impossible situations that can only occur if the program is wrong. They should not be used to detect invalid input (like a bad web request, bad user input, etc). You can use them in production, but the AssertionErrors are, by definition, not designed to be caught.
The root of the issue is that programmers reasonably want some way to assert (in the colloquial sense of the word) properties of values, and it’s reasonable to sometimes recover from those failed assertions. That makes it a natural use case for Exceptions, not Errors. One can sidestep some of this confusion by writing a trivial Exception-throwing alternative to assert
.
Doing the same for pre- and post-conditions involves a bit more effort. Because Clojure doesn’t automatically enforce constraints (whether types or something more expressive) on function arguments the way Java does, those who want to guard against sloppy callers must do so explicitly. Two popular solutions are clojure.spec and Metosin’s malli.
Unfortunately, regardless of how you solve it in your own code, disciplined throwing and catching of Error
is hard to enforce on your dependencies.
Closing thoughts
The idiomatic Clojure approach to error-handling is that there isn’t one. There’s a whole bunch.
Native exceptions, ex-info
, ad-hoc keys, carefully prescribed keys, error maps, chaining Either values – all of these are good idiomatic Clojure. Each is useful in a particular context. They can even coexist in the same project. But – and this is important – there’s no escaping the choice. Sartre was right, we are condemned to be free. Like a good lisp, Clojure leaves it up to you.
— Dave Liepmann, 05 November 2024