Browse Clojure Foundations for Java Developers

Handling Errors and Debugging in the REPL

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:

  • inspect the data that failed
  • inspect the exception value
  • reproduce the smallest failing expression at the REPL
  • instrument only the part of the code path you actually need

That approach is faster than running the whole application again every time you want to answer a small question.

Start With The Exception Report, Not With Guessing

When a form fails at the REPL, read the exception report carefully before changing code.

You usually get three useful clues immediately:

  • the exception type
  • the namespace and line involved
  • the short human-readable message

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.

Use *e Immediately After The Failure

The 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:

  • the exception is still a JVM exception object
  • but the REPL lets you interrogate it as data and keep moving
  • you do not need to restart the whole program just to inspect one failure

Narrow The Failure To One Concrete Value

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.

Prefer Rich Exceptions With ex-info

Java 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:

  • add it exactly where you need it
  • learn what you need
  • remove it once the bug is understood

If you leave ad hoc debugging output everywhere, the codebase becomes noisy quickly.

Recreate The Context Of The Broken Expression

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:

  • in the formula
  • in one incoming value
  • in the assumptions about order of operations

This is much closer to scientific debugging than to “stare at the whole function harder.”

Use Temporary Capture, But Do Not Leave It Behind

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:

  • useful while diagnosing
  • removed once the structure is clear

If the same capture is needed repeatedly, promote it into a proper dev helper or a test fixture instead.

Reach For Tracing Libraries Only When Simpler Tools Stop Helping

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:

  • a call path is too deep for manual prints
  • multiple function boundaries matter
  • you want temporary visibility without rewriting much code

Do not make tracing your first reflex. In many cases, a single captured value tells you more than a wall of trace output.

A Practical Java-To-Clojure Debugging Loop

A disciplined REPL debugging cycle usually looks like this:

  1. run the failing form or reproduce the failing case
  2. inspect *e, ex-data, or the wrong output value
  3. isolate one concrete input or one smaller expression
  4. evaluate subexpressions directly
  5. add temporary print or trace instrumentation only where necessary
  6. move the fix back into source and keep a test if the behavior matters

That 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.

Knowledge Check

### What is usually the best first REPL debugging move after a failure? - [x] Read the exception report and inspect `*e` - [ ] Restart the JVM immediately - [ ] Rewrite the function from scratch - [ ] Add logging to every namespace in the project > **Explanation:** The exception report and `*e` usually tell you the type of failure, where it happened, and what value assumptions broke. ### Why is `ex-info` especially useful in Clojure debugging? - [x] It lets you attach structured domain data to an exception - [ ] It disables stack traces - [ ] It makes all exceptions checked - [ ] It forces Java interop to become type-safe > **Explanation:** `ex-info` supports richer failures because the exception can carry a data map that the REPL can inspect directly with `ex-data`. ### When is a temporary `def` inside a debugging session acceptable? - [x] When you need to capture a value briefly to investigate it, then remove the capture - [ ] As a permanent replacement for tests - [ ] Whenever you do not want to understand the function - [ ] Only in production code > **Explanation:** Temporary capture is useful scaffolding during diagnosis, but it should not become part of the lasting design by accident. ### What is the main benefit of recreating a function's local context at the REPL? - [x] You can evaluate subexpressions directly instead of rerunning the entire code path - [ ] It compiles the function faster - [ ] It removes the need for namespaces - [ ] It automatically fixes stale vars > **Explanation:** Recreating the context with a few `def`s lets you isolate the exact expression or assumption that is failing. ### When do tracing libraries usually make sense? - [x] After simpler value inspection and targeted printing stop being enough - [ ] Before reading the exception message - [ ] Only when using Java classes - [ ] As a permanent replacement for REPL-driven debugging > **Explanation:** Tracing can be useful, but it is usually a second-line tool after smaller and clearer debugging techniques.
Revised on Friday, April 24, 2026