Clojure Stack Traces for the Uninitiated

Clojure Stack Traces for the Uninitiated

Connor Mendenhall
Connor Mendenhall

September 12, 2014

Clojure is one of my favorite languages, and I love pairing with beginners to share the joy. Learning any new language is a process of discovery, and the first steps are usually fraught with syntax errors and failed experiments. Good, fast, dynamic feedback makes learning fun. This is one area where Clojure's REPL shines. But I've seen many newcomers freeze and sometimes give up when their code emits a scary stack trace, interrupting the cycle with a fit of frustration.

Although Clojure's errors are notoriously unfriendly, they're easy to read with a bit of practice, and learning to decipher them is an interesting opportunity to explore the way the Clojure runtime works. Read on, O initiate, and discover the occult secrets of the Clojure stack trace.

Less is more

Consider a simple mistake in Ruby:

def make_sandwich
 :peanut << [:butter, :and, :jelly]


Evaluating this code prints the following concise error:

$ ruby stacktrace.rb
stacktrace.rb:2:in `make_sandwich': undefined method `<<' for :peanut:Symbol (NoMethodError)
 from stacktrace.rb:5:in `<main>'

This is little more than one line, but it's dense with useful information: the Ruby interpreter gives us the file, line number, and method where the error occurred, a short description, and the error's descriptively named class.

Here's a similar mistake in Clojure:

1(ns stacktraces.core)
3(defn make-sandwich []
4 (cons [:butter :and :jelly] :peanut))
6(defn -main []
7 (make-sandwich))

Running this code spews the following foreboding Java stack trace into the terminal:

 1$ lein run
 2Exception in thread "main" java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword, compiling:
 3 at clojure.lang.Compiler.load(
 4 at clojure.lang.Compiler.loadFile(
 5 at clojure.main$load_script.invoke(main.clj:274)
 6 at clojure.main$init_opt.invoke(main.clj:279)
 7 at clojure.main$initialize.invoke(main.clj:307)
 8 at clojure.main$null_opt.invoke(main.clj:342)
 9 at clojure.main$main.doInvoke(main.clj:420)
10 at clojure.lang.RestFn.invoke(
11 at clojure.lang.Var.invoke(
12 at clojure.lang.AFn.applyToHelper(
13 at clojure.lang.Var.applyTo(
14 at clojure.main.main(
15Caused by: java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword
16 at clojure.lang.RT.seqFrom(
17 at clojure.lang.RT.seq(
18 at clojure.lang.RT.cons(
19 at clojure.core$cons.invoke(core.clj:29)
20 at stacktraces.core$make_sandwich.invoke(core.clj:4)
21 at stacktraces.core$_main.invoke(core.clj:7)
22 at clojure.lang.Var.invoke(
23 at user$eval5$fn__7.invoke(form-init6463739264206162333.clj:1)
24 at user$eval5.invoke(form-init6463739264206162333.clj:1)
25 at clojure.lang.Compiler.eval(
26 at clojure.lang.Compiler.eval(
27 at clojure.lang.Compiler.load(
28 ... 11 more

Don't worry! This contains all the same information as the Ruby error above. It's just harder to find, interleaved among noise from the Clojure compiler. We can safely ignore the first 15 lines (for now). Instead, let's take a look at lines 15 and 20-21, which isolate the information we really care about:

Caused by: java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword
 at stacktraces.core$make_sandwich.invoke(core.clj:4)
 at stacktraces.core$_main.invoke(core.clj:7)

There are still a few arcane symbols, but it's better. Mutter an incantation and squint, and you can make out shapes that look like:

  • The file and line that threw the error: core.clj:4
  • The function where it occured: stacktraces.core$make_sandwich
  • A short description: Don't know how to create ISeq from: clojure.lang.Keyword, and
  • The error's class: java.lang.IllegalArgumentException

Learn to filter these four signals from the noise, and you'll master the dark art of reading the Clojure stack trace. Let's take a closer look at this error and explore a few techniques for extracting the meaningful lines.

Find the ultimate cause

The Clojure compiler will usually catch, unwrap, and rethrow exceptions up the call chain from where they originally occur. That means the first few lines of a stack trace won't always contain very useful information. In our case, the proximate cause of the error was an unchecked exception caught in the main thread loading and evaluating our code. But it's the ultimate cause we really care about: we swapped the arguments to cons, which blew up when it tried to use a keyword like a seq.

That error starts all the way down on line 14:

 1Exception in thread "main" java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword, compiling:
 2 at clojure.lang.Compiler.load(
 3 at clojure.lang.Compiler.loadFile(
 4 at clojure.main$load_script.invoke(main.clj:274)
 5 at clojure.main$init_opt.invoke(main.clj:279)
 6 at clojure.main$initialize.invoke(main.clj:307)
 7 at clojure.main$null_opt.invoke(main.clj:342)
 8 at clojure.main$main.doInvoke(main.clj:420)
 9 at clojure.lang.RestFn.invoke(
10 at clojure.lang.Var.invoke(
11 at clojure.lang.AFn.applyToHelper(
12 at clojure.lang.Var.applyTo(
13 at clojure.main.main(
14Caused by: java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword

Exceptions caught and rethrown below the top level will show up as lines starting with "Caused by." When faced with an epic stack trace, start by scanning for these lines and take a closer look at the first one that looks relevant. This one gives us the exception's class and a short description of the error. A good start, but let's zoom in on it to learn more.

Look for your namespace

Now that we've located the ultimate cause of our error, we can decode a few more clues. Between the top level exception and its ultimate cause, there are lots of lines starting with clojure.main, clojure.lang, and clojure.core. Although we are both talented, intelligent programmers, we have probably not discovered a bug in clojure.core or the compiler itself 1. Looking for the first line related to our code's namespace, stacktraces.core, reveals the line and function where our error occurred (see line 10 below):

 1 at clojure.lang.Var.invoke(
 2 at clojure.lang.AFn.applyToHelper(
 3 at clojure.lang.Var.applyTo(
 4 at clojure.main.main(
 5Caused by: java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword
 6 at clojure.lang.RT.seqFrom(
 7 at clojure.lang.RT.seq(
 8 at clojure.lang.RT.cons(
 9 at clojure.core$cons.invoke(core.clj:29)
10 at stacktraces.core$make_sandwich.invoke(core.clj:4)
11 at stacktraces.core$_main.invoke(core.clj:7)

Look at line 4 of stacktraces.core, and sure enough, we'll find our bungled call to cons. When scanning for somewhere to start, begin by filtering out lines related to the compiler and core libraries, and head straight for your own code.

Break out your decoder ring

At this point, we have enough information to uncover our error, but there are deeper mysteries lurking—like the strange dollar sign and invoke nonsense after our namespace in these lines:

1 at stacktraces.core$make_sandwich.invoke(core.clj:4)
2 at stacktraces.core$_main.invoke(core.clj:7)

Although stacktraces.core looks like a Clojure namespace, this line is actually the name of a compiled class. Clojure is a dynamic, compiled language: under the hood, all Clojure code is compiled into Java classes and ultimately JVM bytecode, whether read in and evaluated at the REPL or compiled ahead of time.

The Clojure compiler follows predictable rules when transmuting Clojure symbols into the eldritch runes of the JVM: here, the symbol stacktraces.core/make-sandwich became stacktraces.core$make_sandwich, while stacktraces.core/-main became stacktraces.core$_main. Since these symbols are bound to functions, they're compiled into corresponding classes that implement the .invoke() methods being called here. Find your namespace and look for the dollar sign, and you'll usually find the offending function.

Of course, Clojure supports unnamed functions, which can be trickier: they show up in a stack traces as something like stacktraces.core$_main$fn__19.invoke. When your error is buried deep in an anonymous function, it helps to be able to read the context around it...

Know your abstractions

After spending enough time reading stack traces, certain classes will become familiar friends. Classes that start with clojure.lang are the lowest-level components, written in Java. In our example, clojure.lang.RT contains Java implementations of essential functions like sequence operations and typecasting, while clojure.lang.Var contains fundamental var-related operations (mostly boring) like getting, setting, and implementing .toString(). The code in clojure.lang.* also includes some of the Java abstractions that make Clojure such a joy: interfaces like clojure.lang.IPersistentVector that enable efficient immutable data structures; clojure.lang.IFn, which lets us call maps and vectors like functions; and clojure.lang.ISeq, responsible for sequence operations like first, rest, and cons (and the ultimate source of our error).

Classes that start with clojure.core contain higher level abstractions written in the Clojure language itself, evaluated after clojure.lang classes are used to bootstrap the compiler with the bare necessities implemented in Java. Spending some time learning Clojure's fundamental abstractions—things like sequences, data structures, and protocols will pay off when deciphering a difficult stack trace.

Although Clojure's stack traces seem scary at first, reading them doesn't require any wizardry. It's just a matter of learning where to look, and using a few heuristics to quickly figure out where to start. Use this secret wisdom wisely, and may all your stack traces be short.

See Also

1 If we had, we'd sure be thankful for this enormous stack trace, though!