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:

  1. Scroll down to the Caused by and read the message.
  2. Start from the top again and look for your application or library’s namespaces in the stacktrace.
  3. 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.
  4. 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.
  5. 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

Stackoverflow Link

(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

Stackoverflow link

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


Join the 80/20 DevOps Newsletter

If you're an engineering leader or developer, you should subscribe to my 80/20 DevOps Newsletter. Give me 1 minute of your day, and I'll teach you essential DevOps skills. I cover topics like Kubernetes, AWS, Infrastructure as Code, and more.

Not sure yet? Check out the archive.

Unsubscribe at any time.