Exceptions vs Failure Values, a battle to the death?

This is a blog about the development of Yeller, The Exception Tracker with Answers

Read more about Yeller here

How do I write failures in my clojure code?

Should I use exceptions?

Newcomers to Clojure often ask about how to express “this function call broke”. Various other languages have very strong approaches to this:

  • Java: Use Exceptions
  • Ruby: Use Exceptions (mostly)
  • Python: Use Exceptions
  • Haskell: Use Either and Maybe (mostly)

Clojure takes inspiration from both Java (because it’s hosted on the JVM), and Haskell (it’s all about immutable data and pure functions). As such, we have two main choices for expressing “my code failed in some way”: either we throw an exception, or we return some value that indicates failure.

The Tradeoff

There’s noticeable tradeoffs here. If something breaks and it throws exceptions, you’ll find out pretty quickly. But if something breaks and it returns some “failure” value, the caller has to check for that failure, otherwise errors pass silently. They might lead to (down the line), corrupt data, or pretty much any other kind of undefined behaviour - it gets pretty scary. The latter approach often confuses newcomers to the API or codebase in question.

In good statically typed languages, enforcing “folk getting return values from this function have to check for failures” is much easier - you just return an Either or sum type, that the user has to unwrap to get at the value. Forcing them to do the unwrapping typically involves them having to check the error case, and the compiler enforces it.

parseSomethingThatMightFail :: String -> Either FailureMessage Value
parseSomethingThatMightFail = ...

parseAThingandUseIt raw = case parseSomethingThatMightFail raw of
  (Right value) -> doAThingWith value
  (Left fail) -> logTheFailure fail

But in Clojure, there’s no compiler to help you out there. So you end up with APIs like instaparse, in which you have to explicitly check for failures on parse results, and new users often get confused when code doesn’t throw when it fails to parse a thing:

(defn parse [query now]
 (let [ast (query-parser query)]
  (if (instaparse/failure? ast)
   (log-the-failure ast)
   (transform-parsed-query ast now))))

This api style can work in Clojure. For example, many web form validation libraries use this style. My theory about that is that users of those libraries expect them to fail, and hence are very likely to check for errors. It’s hard to learn how to draw these distinctions though - whilst the expert author of a library might think “of course you should check it’s return type for failure, this is an obvious case for that”, newcomers to that library may not think about checking failure cases at all, only to hit silent bugs in production.

nil: Just Do Not

Don’t return nil for failure (most of the time). Clojure makes nil pretty common, especially when dealing with sequences. And for that use case, it’s ok. But often when interacting with the outside world, returning nil fails to distinguish why the call failed. Very often there’s some useful failure message you can send to the user. Check out what happens if parsing a plist file fails because the file doesn’t exist using this Ruby library:

Plist.parse_xml('some_file_that_doesnt_exist')
=> nil

This call also fails in the exact same way with an invalid file. Or a file that contains unknown xml nodes.

These are all different cases! Consumers of this API shouldn’t have to guess why the api failed.

Do: Use ex-info and ex-data

Clojure makes it relatively more difficult to define your own custom exception types. This is for a good reason - tying in a bunch of types to your API is often more pain than it’s worth. Instead, Clojure users are encouraged to use ex-info, which creates an exception with an arbitary map as data. It has a counterpart ex-data, which returns the map:

(throw (ex-info "some message here" {:some-data 1}))

(ex-data (ex-info "some message here" {:some-data 1})) ; => {:some-data 1}

ex-info and ex-data are a good mechanism for working with exceptions in a manner that meshes well with the rest of Clojure. There’s no rigid type hierarchy that your callers are coupled to, and you can easily add arbitrary context to help your callers debug things. They present a nice middle ground between exceptions and failure values.

Do: Enforce a programmatic means of distinguishing between kinds of errors

Go (mostly) restricts the user to using error values, as opposed to exceptions. This is an approach (with upsides and downsides detailed above), and the static typing means it’s relatively easy to enforce good error handling. But one place where it really messes up is that there’s no way (that’s present and spread throughout the library) to distinguish different types of errors - they’re all just a type with a message string.

As a result, you end up with code that has to grep for those human readable messages to determine what the cause of the error was. Was this a file that didn’t exist? Was it a failed http request? The only mechanism for determining the different (as built in) is to examine the error message. This is just stringly typed programming at it’s worst.

With ex-info and ex-data, you can take a very flexible approach to this: just add an :error-type keyword to the data, and put a domain specific failure type that’s easy to work with programatically. clj-http does this very well, dropping the http status code and a failure message in the ex-data. For your own exceptions, internal to your app, I like using a keyword in there, like so:

(throw (ex-info "a human readable message") {:error-type :some-easily-consumed-by-machine-data})

How To Apply This to your App

  • Use Exceptions where appropriate
  • Work hard to prevent errors passing silently
  • Use ex-info and ex-data when throwing your own exceptions
  • Enforce a programmatic distinction between kinds of errors

Error handling is one of the harder parts of day to day programming. Get it right, and your system is easier to debug and more robust. Get it wrong and your system is actually broken. Hopefully this post gives you some pointers, some things to avoid, and some ways to improve.

This is a blog about the development of Yeller, the Exception Tracker with Answers.

Read more about Yeller here

Looking for more about running production applications, debugging, Clojure development and distributed systems? Subscribe to our newsletter: