How To Understand Clojure Stacktraces And Error Messages
We’ve all been there. You’re hacking away on some Clojure code, making a few changes and then try to run the code in your REPL. Instead of getting the result you wanted and your tests passing, you get your worst nightmare:
UnsupportedOperationException nth not supported on this type: IPersistentMap
And the first thoughts that pop into your mind are:
I’m not calling nth
anywhere! What the hell?
Then you google ’nth not supported on this type: IPersistentMap’ and don’t get anything particularly useful and you proceed to bisect your code manually to find the problem.
This should sound familar to any Clojure developer because we’ve all been there when we were starting out.
By the way, this is one of my all time favorite Clojure errors. I’m not sure why. Probably because it happens so frequently to me and I went through that exact thought process described above so many times.
But now, when I see that error, I know it’s extremely likely I messed up destructuring.
I promise you that with a little practice you can learn how to read and interpret the error messages like a pro. You’ll be able to develop heuristics and instantly know what to look for.
You’re not alone. The biggest frustration for Clojure programmers 3 years in a row is the error messages.
I do have some good news for you.
Expect error messages in Clojure to become far better in the some-what near future. The Clojure team is planning to write specs for the clojure.core macros, which should dramatically improve error messages.
Until then, I hope this guide will alleviate some of those pains.
Reading Stacktraces
I’m going to first take you through how to read stacktraces. I’ve looked on Stackoverflow for some to look at.
Let’s use this one as our first example:
(defn dodgy-map []
{:1 :2 :3})
$ lein ring server-headless
Exception in thread "main" java.lang.RuntimeException: Map literal must contain an even number of forms, compiling:(one_man_wiki/views.clj:19)
at clojure.lang.Compiler.load(Compiler.java:6958)
at clojure.lang.RT.loadResourceScript(RT.java:359)
at clojure.lang.RT.loadResourceScript(RT.java:350)
at clojure.lang.RT.load(RT.java:429)
at clojure.lang.RT.load(RT.java:400)
at clojure.core$load$fn__4890.invoke(core.clj:5415)
at clojure.core$load.doInvoke(core.clj:5414)
at clojure.lang.RestFn.invoke(RestFn.java:408)
at clojure.core$load_one.invoke(core.clj:5227)
at clojure.core$load_lib.doInvoke(core.clj:5264)
at clojure.lang.RestFn.applyTo(RestFn.java:142)
at clojure.core$apply.invoke(core.clj:603)
at clojure.core$load_libs.doInvoke(core.clj:5298)
at clojure.lang.RestFn.applyTo(RestFn.java:137)
at clojure.core$apply.invoke(core.clj:603)
at clojure.core$require.doInvoke(core.clj:5381)
at clojure.lang.RestFn.invoke(RestFn.java:457)
at one_man_wiki.handler$eval1564$loading__4784__auto____1565.invoke(handler.clj:1)
at one_man_wiki.handler$eval1564.invoke(handler.clj:1)
at clojure.lang.Compiler.eval(Compiler.java:6511)
at clojure.lang.Compiler.eval(Compiler.java:6501)
at clojure.lang.Compiler.load(Compiler.java:6952)
at clojure.lang.RT.loadResourceScript(RT.java:359)
at clojure.lang.RT.loadResourceScript(RT.java:350)
at clojure.lang.RT.load(RT.java:429)
at clojure.lang.RT.load(RT.java:400)
at clojure.core$load$fn__4890.invoke(core.clj:5415)
at clojure.core$load.doInvoke(core.clj:5414)
at clojure.lang.RestFn.invoke(RestFn.java:408)
at clojure.core$load_one.invoke(core.clj:5227)
at clojure.core$load_lib.doInvoke(core.clj:5264)
at clojure.lang.RestFn.applyTo(RestFn.java:142)
at clojure.core$apply.invoke(core.clj:603)
at clojure.core$load_libs.doInvoke(core.clj:5298)
at clojure.lang.RestFn.applyTo(RestFn.java:137)
at clojure.core$apply.invoke(core.clj:603)
at clojure.core$require.doInvoke(core.clj:5381)
at clojure.lang.RestFn.invoke(RestFn.java:421)
at user$eval1.invoke(NO_SOURCE_FILE:1)
at clojure.lang.Compiler.eval(Compiler.java:6511)
at clojure.lang.Compiler.eval(Compiler.java:6500)
at clojure.lang.Compiler.eval(Compiler.java:6477)
at clojure.core$eval.invoke(core.clj:2797)
at clojure.main$eval_opt.invoke(main.clj:297)
at clojure.main$initialize.invoke(main.clj:316)
at clojure.main$null_opt.invoke(main.clj:349)
at clojure.main$main.doInvoke(main.clj:427)
at clojure.lang.RestFn.invoke(RestFn.java:421)
at clojure.lang.Var.invoke(Var.java:419)
at clojure.lang.AFn.applyToHelper(AFn.java:163)
at clojure.lang.Var.applyTo(Var.java:532)
at clojure.main.main(main.java:37)
Caused by: java.lang.RuntimeException: Map literal must contain an even number of forms
at clojure.lang.Util.runtimeException(Util.java:170)
at clojure.lang.LispReader$MapReader.invoke(LispReader.java:1071)
at clojure.lang.LispReader.readDelimitedList(LispReader.java:1126)
at clojure.lang.LispReader$ListReader.invoke(LispReader.java:962)
at clojure.lang.LispReader.read(LispReader.java:180)
at clojure.lang.Compiler.load(Compiler.java:6949)
... 51 more
What do you notice about it?
I’d wager that the first thing you say to yourself is that this is a long stacktrace.
Luckily for you, you can ignore 95% of it. When you get more experience with Clojure, you can tune out the noise and focus in on the signal in stacktrace.
The information that I care about the majority of the times is right here:
Exception in thread "main" java.lang.RuntimeException: Map literal must contain an even number of forms, compiling:(one_man_wiki/views.clj:19)
[snip]
at one_man_wiki.handler$eval1564$loading__4784__auto____1565.invoke(handler.clj:1)
at one_man_wiki.handler$eval1564.invoke(handler.clj:1)
[snip]
at user$eval1.invoke(NO_SOURCE_FILE:1)
[snip]
Caused by: java.lang.RuntimeException: Map literal must contain an even number of forms
[snip]
That’s it.
The vast majority of times, you can tune out everything that’s not in your application or library’s namespace.
In this person’s case, it should be simple because Clojure tells you which namespace and line the error occurs on. And I would hope this particular error message is at least understandable.
If not, let’s pick it apart.
First go straight to the Caused by
and read it:
Map literal must contain an even number of forms
Then go back to the top and scan down until you find a line with your application’s namespace. If it has a line number, great! Examine that line in your editor.
For the very beginners, it’ll take a little time to get used to the way that Clojure describes errors.
But you should be able to recognize that you’re using a map literal:
{:a :b :c}
and see that it indeed does not have an even amount of
forms because you have three elements in the map and the compiler
wants every key to be paired with a value. In this case, the :c
key
has nothing coming after it.
If the first line you find in your application’s namespace doesn’t help you, go to the next and repeat.
The Clojure Debugging Algorithm
To summarize the above process in a more generalized form:
- Scroll down to the
Caused by
and read the message. - Start from the top again and look for your application or library’s namespaces in the stacktrace.
- Look for line numbers. If you get a
NO_SOURCE_FILE
where your line numbers should be then that means you executed your code inside your REPL prompt without an associated file. Run your code in a file. - Did you get a line number? Go inspect your code there and see if the error message starts making sense. If it’s not in your application’s code and instead in a library you use, then you’ll need to read the source of that library. Your editor should be able to jump to the definition of that symbol. I know Intellij/Cursive and Emacs/Cider can.
- If you haven’t got an idea how to fix your problem, repeat step 2 on the next line of your stacktrace.
Example Stacktraces And Error Messages
Let’s try to go through some error messages I grabbed from Stackoverflow.
java.lang.ClassCastException: java.lang.Long cannot be cast to
clojure.lang.IPersistentCollection
You’re trying to pass a Long (number) to something that is expecting a collection.
The idea of java.lang.ClassCastException
is that you’re giving a
function or method a type it can’t work with.
It goes in the form of:
java.lang.ClassCastException: X cannot be cast to Y
Type Y was expected, but type X was given, so the JVM is trying to cast X to be Y type. In this case it’s not possible because the JVM doesn’t know how to turn a Long (number) into a collection.
In cases like this when you see something in the pattern of
clojure.lang.I<something>
, you’re dealing with a java
interface. Here, we’re talking about
IPersistentCollection.
Now, reading the java source of interface won’t really help you
here. But it tells you that your java.lang.Long
doesn’t implement
the IPersistentCollection
interface and methods.
Unbound Vars
java.lang.ClassCastException: clojure.lang.Var$Unbound cannot be cast to clojure.lang.Atom
This one will be tougher if you’re not familiar with
clojure.lang.Var$Unbound
’s. But some Googling should give you a good
idea. When you see this error with Var$Unbound
, it means you have a
dynamic var that isn’t bound in the thread of execution. Dynamic vars
are usually indicated with ’earmuffs’ and the dynamic
metadata.
(def ^:dynamic *request-id*)
Wrong Number Of Args
(ns net.projecteuler.problem31)
;; elided
(defn make-combination-counter [coin-values]
(fn recurse
([sum] (recurse sum 0 '()))
([max-sum current-sum coin-path]
(if (= max-sum current-sum)
; if we've recursed to the bottom, add current path to paths
(dosync (ref-set paths (conj @paths (sort coin-path))))
; else go on recursing
(apply-if (fn [x] (<= (+ current-sum x) max-sum))
(fn [x] (recurse max-sum (+ x current-sum) (cons x coin-path)))
coin-values)))))
;; Error: Wrong number of args passed to: problem31$eval--25$make-combination-counter--27$recurse--29$fn (NO_SOURCE_FILE:0)>
This is a scarier looking one.
First, we see NO_SOURCE_FILE
, so if you wanted the line number then
you need to evaluate it in a source file and not directly in your
REPL.
Second, find our application’s namespace. In this case,
problem31
. But instead of a neat looking call, it has all these
dollar signs and dashes in them.
That’s usually due to using anonymous functions.
Did you know you can name your anonymous functions? If you didn’t name
them, you’d see a bunch of fn
’s and dashes. In this case, the author
named the recurse
anonymous function so we see that at the end of
the error. But he didn’t name the inner-most anonymous function so he
got an fn
. Tracing through that should tell you almost exactly where
the problem is. Using the error message as a clue, we can see that he
is passing the wrong number of args to an anonymous function nested
inside the recurse
function. I’d then just look for function calls
and log out the outputs or use my debugger to examine the args.
No Implementation Of Method
java.lang.IllegalArgumentException: No implementation of method: :make-reader of protocol: #'clojure.java.io/IOFactory found for class: nil
No implementation of method
errors means that the object you are
calling a method on does not have that method. Usually because you
didn’t implement the method on that type or that object is nil
, like
in this case.
No Such Var
Exception in thread "main" java.lang.RuntimeException: No such var: db/get-user, compiling:(guestbook/routes/auth.clj:38:14)
No such var
means that the Clojure compiler can’t find the var
you’re referring to. Either this means you didn’t refer to the correct
namespace or it isn’t defined. Usually the former case.
nth Not Supported On This Type
Back to my favorite error:
UnsupportedOperationException nth not supported on this type: IPersistentMap
Whenever you see nth
not supported on a type, what you want to look
for generally is destructuring. If you’re doing vector destructuring:
(let [[a b c] :not-an-nthable-collection]
:do-stuff)
Check that what you’re destructuring is a vector or a list.
Essentially, Clojure is trying to call nth
on
:not-an-nthable-collection
to get the first three values and can’t
while destructuring with let
. This is one of those errors that will
get better with more spec’d macros.
Minimizing Errors
While you can never avoid Clojure’s error messages entirely, you can certainly adjust your process to minimize them.
I recommend making small, incremental changes and evaluating them/running tests right away so when something breaks the surface area will be small.
Finally, in Clojure 1.9.0 alpha14 there are already a few macros (like
ns
) that have specs implemented. The error messages are much better!
If you can get away with using an alpha version of Clojure 1.9.0 then it’ll alleviate some of the bad error messages.
Resources
Master GitHub Actions with a Senior Infrastructure Engineer
As a senior staff infrastructure engineer, I share exclusive, behind-the-scenes insights that you won't find anywhere else. Get the strategies and techniques I've used to save companies $500k in CI costs and transform teams with GitOps best practices—delivered straight to your inbox.
Not sure yet? Check out the archive.
Unsubscribe at any time.