CDT - The Clojure Debugging Toolkit

What is it?

The CDT, (Clojure Debugging Toolkit,) is a set of clojure functions/macros that use the Java Debug Interface,, to debug a remote vm, from a repl running on another vm.

It contains a command line based debugger and library, which can be integrated with other GUI's, e.g. swank-cdt.

You can set breakpoints, catch exceptions, examine the stack frame/locals; what makes it unique, (afaik,) is that you can eval arbitrary clojure forms in the lexical scope of a suspended, remote-thread's stack frame.

For example, if you are suspended at a stack frame that has the locals a and b, (reval (ct) (cf) (+ a b)) will return their sum.

What about debug-repl?

The debug-repl,, is a dead simple interface that allows you to debug Clojure in the most natural way possible, a repl that is aware of it's surrounding lexical scope.

In general it works very well, with two exceptions:

1. You can't traverse the stack examining locals.

2. It can be hard to invoke from the point where an exception is thrown.

For those kinds of issues I use CDT; otherwise, I often use debug-repl, depending on what is more convenient.

CDT Exception Example

I often have trouble getting my namespace declarations right. I do things like this:

user=> (ns xx (:use [clojure.pprint :only pprint]))
java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol (NO_SOURCE_FILE:16)

Any idea what symbol it is complaining about? Me neither. To debug it, start the target repl up with these extra jvm args:


Start up a second repl and init the debugger like so:

user=> (use 'cdt.ui)
user=> (cdt-attach 8030)
starting event handler
user=> (set-catch java.lang.IllegalArgumentException :all)
catch set on java.lang.IllegalArgumentException

Now that you are catching the exception, reinvoke it from the target repl again; that repl will hang, without printing the exception. On the debugger repl, run the following:


Exception #<ExceptionEventImpl ExceptionEvent@clojure.lang.RT:471 in thread main> #<LocationImpl clojure.core$load_lib:4752> hit

(print-frames (ct))
  0 clojure.lang.RT seqFrom [c coll sc]
  1 clojure.lang.RT seq [coll]
  2 clojure.core$seq invoke [coll this] core.clj:133
  3 clojure.core$refer doInvoke [exclude filters fs ns ns-sym nspublics rename this to-do] core.clj:3767
  4 clojure.lang.RestFn applyTo [--site--0-- --site--1-- --site--2-- --thunk--0-- --thunk--1-- --thunk--2-- args const--0 const--1 const--10 const--11 const--12 const--13 const--14 const--15 const--16 const--17 const--18 const--19 const--2 const--20 const--21 const--22 const--23 const--24 const--25 const--26 const--27 const--28 const--29 const--3 const--30 const--4 const--5 const--6 const--7 const--8 const--9 this]
  5 clojure.core$apply invoke [args f this x] core.clj:602
  6 clojure.core$load_lib doInvoke [as filter-opts lib load loaded map--4561 need-ns options opts prefix reload reload-all require this use verbose] core.clj:5252
  7 clojure.lang.RestFn applyTo [args const--0 const--1 const--10 const--11 const--12 const--13 const--14 const--15 const--16 const--17 const--18 const--19 const--2 const--20 const--21 const--22 const--23 const--24 const--25 const--26 const--27 const--28 const--29 const--3 const--30 const--31 const--32 const--33 const--34 const--35 const--36 const--37 const--38 const--39 const--4 const--40 const--41 const--42 const--43 const--44 const--45 const--46 const--47 const--48 const--49 const--5 const--50 const--51 const--52 const--53 const--54 const--55 const--56 const--6 const--7 const--8 const--9 this]

user=> (locals (ct) (cf))
coll pprint
sc clojure.lang.AFn
c clojure.lang.Symbol
user=> (reval (ct) (cf) (type coll))


Suspiciously, there is a local coll whose value is pprint. (reval (ct) (cf) (type coll)) reports it's a symbol. Sounds like that could be the ISeq the exception message is complaining about. Delete the catch and resume the target:

user=> (delete-catch java.lang.IllegalArgumentException )
user=> (continue-thread (ct))

Wrap the pprint in a vector and see that it fixes the problem.

Caveat - False Nulls/Locals Clearing?

One major weakness of CDT I've found is that sometimes valid non-null locals appear null. I've seen this problem with JDB as well, so I don't think it is a CDT problem per se; I suspect it's an unpleasant side-effect of the "locals clearing" the compiler does to reduce the danger of head-holding lazy seqs:

I'm not sure there's a workaround, but if you go up or down the stack frame you can sometimes find other copies of the var that actually do show its correct value.

Other useful commands

(up (ct) (cf)) and (down (ct) (cf)) traverse the stack.

Set/delete breakpoints like so: (set-bp clojure.core/into) (delete-bp clojure.core/into)

Other caveats

Dynamic bindings are only correct in frame 0

reval is always invoked in the context of frame 0 on a suspended thread. The lexical scope for other frames is handled by pulling them out of the jdi and passing them back into reval when it is invoked. Because dynamic bindings are a clojure construct, the jdi doesn't know when they get set, so they can't be simulated. Thus reval'ing a form that depends on a dynamic binding will only be correct in frame 0.

Locals from Java source files

are only available when compiled with -g

Haven't tested on Windows

Outside of the caveats mentioned above, CDT seems to work fine on Linux and OSX.



To Rich for making all the great toys.

YourKit is kindly supporting open source projects with its full-featured Java Profiler. YourKit, LLC is the creator of innovative and intelligent tools for profiling Java and .NET applications. Take a look at YourKit's leading software products: YourKit Java Profiler and YourKit .NET Profiler.


Send any comments/suggestions to George Jahad at "george-clojure at" or to the main clojure mailing list: