Improving error messages in Clojure (as a library)?

The issues with Clojure error messages are numerous and as I’ve said before, I think it’s important to try to tease them apart. Some of the things I have thought about for a long time:

  • Message context
    • Invalid values often propagate through the outer clojure.core layer into the JVM layer before they blow up. That code is often either very generic or lacking outer context. My experiments with clojure.core function specs significantly improve this - the error is caught at the first function layer and an error can be throw in the user’s call with location information, the bad predicate, the value, etc.
    • Macros obscuring context - given one or more layers of macro expansion an error sometimes occurs in expanded code, which exists neither in the user’s code nor in the macros
    • Missing language context - I see a class of complaints that are really about not having a good mental model for how some part of Clojure works. That’s not the user’s fault. Probably a relatively small set of these could be made gentler and make a big difference (like trying to invoke things that aren’t functions). In any case, having a means to integrate additional information would be useful. I don’t imagine that information would be “in core” but we could provide the hooks.
  • Conveyance
    • There is only one exception type that conveys error location (file/line/col) right now: Compiler$CompilerException. Forcing all location wrapping through this single inner class is confusing and at odds with the better support we now have in things like ex-data. Due to this, CompilerException is often used to wrap better errors, merely to provide location information.
    • There needs to be a distinction made between “your invocation is invalid Clojure code”, “your invocation produced an exception in your code”, “Clojure compiler error”, “Clojure runtime error”, etc. Maybe not exactly those, but a handful of broad categories. At the point of throw or rethrow, the general category is known and Clojure should help more here (so that tools can help more on the receiving end).
    • Code has a context (local bindings etc) and we know a lot of it. Conveying more of that could be put to great use.
  • Reporting
    • Due to the CompilerException wrapping above, there is often a chain being sent purely for location conveyance. Tools generally don’t unwrap these exceptions, but they should. Cursive for example shows you only the top error, the CompilerException, which has the location information, but usually a less useful error message and a useless stack trace. It is almost always more useful to look at the root cause exception - the message and stack are almost always what you actually want (but missing the location info!)
    • Unnecessary stack traces - some (maybe most) of the categories above don’t need actually need a stack trace at all. If you had the right info for a language error or spec error, you can just tell the user the thing they did and why it’s wrong (and maybe how to fix it).
    • Unnecessary frames - when stack traces are emitted, there is often a lot of noise and many people have taken stabs at simplifying. I find I hate most of the “simplified” stack trace printers but I have hope that there are built-in improvements to be made.
  • Spec
    • We are well aware that the core spec macro errors are intimidating and in some cases worse than prior behavior. (However, they also catch and fail on many previously accepted invalid inputs.) The spec macros like ns and defn have a) wide fanout (like the ns clauses), b) complicated structure (destructuring) and/or c) recursion (destructuring). In many ways, these represent the hardest possible cases for automatic error reporting. I think there are many many ways these messages can be greatly improved for these hard cases. I think people miss that the “easy” cases are really pretty good a lot of the time (since we don’t have the simpler core function specs included yet).
    • expound has done a great job building on the explain-data to go really far in making spec messages super friendly. Our goals in the core language differ somewhat from expound and while I do expect the default printer to get much better, I don’t expect core to end up where expound is now. In particular, there seems to be a lot of pressure for customizing errors and I actually think there is way more room to make the non-custom cases really good, and there is a ton of benefit in that.
    • I think there are still vast opportunities for tools to leverage specs and explain-data visually in novel ways beyond what expound can do. I think we can do more in spec in the future to provide some help here.

I’ve got some other writeups on this stuff and I probably forgot some things but that’s a good start.

34 Likes