Welcome! Please see the About page for a little more info on how this works.

+2 votes
in Errors by

Clojure stacktraces are pretty cryptic and hard to follow, is there a library to display better errors or format the stacktraces?

2 Answers

+1 vote

There are a number of libraries available to alter the printing of stacktraces offering things like collapsing, clojure framing, and colorization. We do not currently have any planned work on this in the next Clojure release.

Can you point me which one are these libraries?
Michel, if you're not already on Clojure 1.10.1, I'd recommend upgrading. A lot of work on error messages was done in 1.10 and a better reporting mechanism was added in 1.10.1.

I'll also just drop a link to Stuart Halloway's Twitter thread about stacktraces: https://twitter.com/stuarthalloway/status/1148295437448876032

Stacktraces may be intimidating when you're getting started with Clojure but learning to read them is worth the effort and, to be honest, collapsing/eliding them is nowhere near as helpful as you might think (because sometimes the important information is in a piece that gets hidden by such libraries).
When I heard about better error messages this is what I thought was going to happen in 1.10:

Errors like '''ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn''' are cryptic for new comers see: https://stackoverflow.com/questions/26720847/classcastexception-java-lang-long-cannot-be-cast-to-clojure-lang-ifn

For me the value add here is to be able to point to the column and row of the error in the user's source code to quickly show where the error is in the nested thing

And then to do a better job of explaining the why of the error occurred in Clojure's terms rather than Java's I don't really want to hear about classes if as far as I'm concerned there are no classes in my code i.e. (1 2 3)

All that said I'm definitely naive to how plausible this is we have heavy interop in this language which may make this a lot harder than in a closed system like elm
Clojure _could_ detect these conditions and throw more informative exceptions but that introduces a performance overhead for everyone. The historical choice has always been to go faster and suffer "unusual" Java exceptions since each newcomer only has to learn these "once" -- so it's a good trade-off for the vast majority of users.

That said, it would be nice if there was a "friendly" REPL option built-in that used :caught to provide more friendly error messages.
As an example:

user=> (defn easy [t]
                  (if (re-find #"cannot be cast to clojure.lang.IFn" (ex-message t))
                    (ex-info (str "Expected a function -- found: " (second (re-find #"^([^ ]*)" (ex-message t)))) {:cause t})
user=> (clojure.main/repl :caught easy)
user=> (1 2 3)
Execution error (ExceptionInfo) at user/easy (REPL:1).
Expected a function -- found: java.lang.Long
+1 vote

Here's another +1 for even better error message out of the box.

The libraries mentioned below are treating the symptoms more or less successfully, but let's look at it from the point of someone wanting to learn Clojure, hence heads to Clojure.org, clicks on Getting Started.

Ok, I now know how to download it. Maybe I'll try to get a feel for it by using repl.it.
Now I have the bare bones experience (with the - according to clojure.org confusing error messages) (see : "One confusing error you might see is the result of accidentally trying to evaluate a list of data as if it were code:" )

Well, let's go to Learn Clojure first.
Ok, still nothing about how to launch a REPL.
Third link: Good idea to start a REPL.
Fourth link: Here are the n ways to start a REPL.
Here are other ways to run a REPL.

None mention anything about: If you are a beginner, let's add clj-stacktrace, then start a REPL.
Do you see why this is impractical?

I know you have put a lot of effort into better docs in recent years and I feel bad pointing out these things. It is hard coming back to something so familiar and look at it through the eyes of a developer who wants to learn about this new language.

I believe you have 1 or maybe 2 shots at making a good impressions and readable error messages that tell you where you went wrong is an important part of that.

user=> (map 1 inc)
Don't know how to create ISeq from: clojure.core$inc:

user=> (1 2 3)
Execution error (ClassCastException) at user/eval7 (REPL:1). class java.lang.Long cannot be cast to class clojure.lang.IFn (java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')

Both don't tell me what I did wrong. It tells me some specifics of how the lisp evaluator works behind the scenes. (some of them are documented on clojure.org).

Since none of this is helpful for me, the new user, I'm going to google it.
The tolerance for googling error messages for errors like wrong argument order or invoking something that can't be invoked, is not very high for beginners.

That is where you leave the impression that error messages guiding the user are not important.
(btw, curiously, you are already handing (nil 2 3) pretty good and not throwing an NPE)

This is where clojure (the implementation) actively contributes to the 'huge learning curve' meme.
It doesn't matter that - after seeing this error often enough - I learned what it is. (with regards to Sean's comment).

The damage is done.

Other Clojure dialects are doing a better job here. (CLJS and Sci, for example).

Speaking of effort: There's an objection that providing better error messages would impact performance. If we are talking about exception handling here (and in both cases above we do),
I'm not sure I understand where the performance impact is.
Exception handling because an s-expr fails is not on the performance-critical path, I would think.
Happy to better understand where the performance aspect comes into play.

Also, just for the two sample error messages above, the effort to write them from the perspective of the user - not the system - is not very high. For one of them it means changing line 557 in RT.java

Better error messages might mean catching lower-level exceptions, associating them with the current AST and re-throwing them with a message associated with the list element that generated the error.

I would implore you to guide beginners to the very best REPL they can get.
Make that the default in Getting Started and Learning Clojure.
Then you can draw back the curtain and say: Ya know, underneath is IFn and ISeq and you might see Java stacktraces.

There are so many new concepts to learn and understand when going down the Clojure path. Re-interpreting error messages shouldn't be one of them.

And here's yet another clj first-timer tripping over ClassCastException:

"Ok, still nothing about how to launch a REPL." -- I realized (after seeing your recent comment on this answer) that the installation instructions do not mention what to do after installation, so I submitted a PR on the Getting Started page to add a line about running clj or clojure after installation in order to run a REPL. Alex merged it so I hope that's an improvement, albeit a small one.
Thank you, Sean.
It is not trivial to re-read documentation from the standpoint of a newcomer that is not familiar with concepts that are second nature now.

In that same vein, maybe it would be useful to collect feedback about the documentation directly on the page. Something many companies do when trying to improve documentation aimed at non-experts. Maybe a feedback button with a slightly better version that this: https://docs-feedback.aws.amazon.com/feedback.jsp?feedback_destination_id=068c0217-1615-4028-b3f7-7de2fb227b77&topic_url=https://aws.amazon.com/getting-started/hands-on/build-web-app-s3-lambda-api-gateway-dynamodb/module-one/
(as an example)