Use the REPL to read exception data, recreate failing contexts, and inspect values directly instead of guessing.
Java developers often arrive in Clojure expecting debugging to start with breakpoints and stack-frame inspection.
Those tools still exist on the JVM, but Clojure pushes you toward a different first move:
That approach is faster than running the whole application again every time you want to answer a small question.
When a form fails at the REPL, read the exception report carefully before changing code.
You usually get three useful clues immediately:
For example:
1(defn line-item-total [{:keys [qty unit-price]}]
2 (* qty unit-price))
3
4(defn order-total [order]
5 (->> (:items order)
6 (map line-item-total)
7 (reduce +)))
8
9(def broken-order
10 {:order/id 42
11 :items [{:sku "A-1" :qty 2 :unit-price 15M}
12 {:sku "B-2" :qty "3" :unit-price 9M}]})
13
14(order-total broken-order)
15;; => Execution error (ClassCastException) ...
That already tells you something important: the failure is not “somewhere in Clojure.” It happened because one of the values flowing through line-item-total is not shaped the way the function expects.
*e Immediately After The FailureThe last exception is available as *e.
That lets you keep investigating without rerunning the broken form blindly:
1(.getMessage *e)
2;; => "class java.lang.String cannot be cast to class java.lang.Number"
If you want a fuller stack trace, use pst, which prints the stack trace for *e by default:
1(require '[clojure.repl :refer [pst]])
2
3(pst *e)
For Java developers, the important shift is this:
Once you know a function blew up, stop testing the whole pipeline and isolate the specific value:
1(def bad-item (second (:items broken-order)))
2
3bad-item
4;; => {:sku "B-2", :qty "3", :unit-price 9M}
5
6(line-item-total bad-item)
7;; => Execution error ...
That is already a better debugging state than “my order calculation failed.”
Now you can ask smaller questions:
1(type (:qty bad-item))
2;; => java.lang.String
3
4(number? (:qty bad-item))
5;; => false
This is one of the biggest REPL advantages over a traditional compile-run cycle: once the bad input is captured, you can inspect it from multiple angles with almost no overhead.
ex-infoJava developers are used to exception types carrying meaning. Clojure keeps that idea, but it also makes it easy to attach structured data to failures with ex-info.
1(defn validate-item [{:keys [qty] :as item}]
2 (when-not (number? qty)
3 (throw
4 (ex-info "Line item quantity must be numeric"
5 {:field :qty
6 :item item})))
7 item)
8
9(try
10 (validate-item bad-item)
11 (catch Exception e
12 (ex-data e)))
13;; => {:field :qty, :item {:sku "B-2", :qty "3", :unit-price 9M}}
This is idiomatic Clojure debugging because the error can carry the exact domain context you need, instead of forcing you to reconstruct it from log text later.
At the REPL, ex-data is often more useful than the full stack trace.
Sometimes the problem is not an exception. The code runs, but the answer is wrong.
In those cases, printing intermediate values is often enough:
1(defn order-total-with-debug [order]
2 (->> (:items order)
3 (map (fn [item]
4 (doto item prn)
5 (line-item-total item)))
6 (reduce +)))
prn is usually better than println when debugging values because it prints data in a reader-friendly form.
Keep this style of instrumentation temporary and surgical:
If you leave ad hoc debugging output everywhere, the codebase becomes noisy quickly.
A classic REPL debugging move is to rebuild the local context of a failing expression with a few defs.
Suppose this function gives the wrong result:
1(defn discounted-total [subtotal discount-rate tax-rate]
2 (+ (* subtotal (- 1 discount-rate))
3 (* subtotal tax-rate)))
Instead of repeatedly calling the whole function and mentally simulating the body, recreate the inputs:
1(def subtotal 100M)
2(def discount-rate 0.20M)
3(def tax-rate 0.13M)
4
5(* subtotal (- 1 discount-rate))
6;; => 80.00M
7
8(* subtotal tax-rate)
9;; => 13.00M
Now you can test each subexpression directly and decide whether the bug is:
This is much closer to scientific debugging than to “stare at the whole function harder.”
Sometimes you need to save an in-flight value from inside a larger function. A temporary def can help:
1(defn order-total-capturing [order]
2 (let [items (:items order)]
3 (def captured-items items) ; remove after debugging
4 (->> items
5 (map line-item-total)
6 (reduce +))))
That is useful during investigation, but it is not a design pattern.
Treat temporary captures like scaffolding:
If the same capture is needed repeatedly, promote it into a proper dev helper or a test fixture instead.
The Clojure REPL guides emphasize lightweight techniques first: inspect values, print a few intermediates, recreate context.
That should be your default.
When you need broader instrumentation, tracing libraries can help. clojure.tools.trace, for example, can wrap expressions or trace namespaces:
1(require '[clojure.tools.trace :refer [trace]])
2
3(defn subtotal [items]
4 (trace :subtotal
5 (reduce + (map line-item-total items))))
Use tracing when:
Do not make tracing your first reflex. In many cases, a single captured value tells you more than a wall of trace output.
A disciplined REPL debugging cycle usually looks like this:
*e, ex-data, or the wrong output valueThat loop is what makes REPL debugging feel different from Java debugging in an IDE. You are not just stepping through control flow. You are interrogating the program as a running system of values.