Debug Clojure inside Java systems by reading stack traces, inspecting data at the REPL, using ex-info for contextual failures, and separating domain outcomes from broken system conditions.
Debugging Clojure in a Java system is less mysterious when you remember that both languages run on the JVM. Stack traces, exceptions, logging, profilers, and thread dumps still matter. What changes is how much of the code can be inspected as data and re-run at the REPL.
The best migration debugging strategy is to make the Clojure slice small, pure, and observable before it owns production effects.
When Clojure fails inside Java, first locate the boundary.
| Failure location | Likely cause and first check |
|---|---|
| Java caller before Clojure invocation | Adapter wiring, namespace loading, classpath, or feature flag. Confirm the Clojure namespace is loaded and the function var is resolved. |
| Java-to-Clojure conversion | Nulls, money values, dates, enums, or collections. Inspect the map passed into the Clojure function. |
| Pure Clojure function | Business rule, missing key, invalid shape, or numeric behavior. Re-run the function at the REPL with the captured input. |
| Clojure-to-Java conversion | Missing output key or unexpected result type. Validate the output contract before converting. |
| Effect boundary | Database, queue, email, HTTP, retry, or timeout. Confirm the pure decision and effect command separately. |
This boundary-first approach prevents random debugging. It tells you whether the bug is in orchestration, conversion, decision logic, or effects.
Instead of stepping through a large service graph, capture the input map that reached the Clojure function.
1(def failing-input
2 {:customer {:customer/id "C-42"
3 :customer/credit-limit 1000M}
4 :orders [{:order/id "O-1"
5 :order/outstanding-amount 250M
6 :order/past-due? true}
7 {:order/id "O-2"
8 :order/outstanding-amount nil
9 :order/past-due? false}]})
Now the failure can be reproduced without the controller, repository, transaction, and email service.
1(account-summary (:customer failing-input)
2 (:orders failing-input))
If this fails, the team can decide whether nil is invalid input, a conversion bug, or a domain case the function should handle.
Java developers are used to exception classes carrying meaning. In Clojure, ex-info lets you attach structured data to a failure.
1(defn require-money [value path]
2 (if (some? value)
3 value
4 (throw (ex-info "Missing money value"
5 {:error/type :invalid-input
6 :error/path path}))))
Java can still catch the exception. Clojure code can inspect the data with ex-data.
1(try
2 (require-money nil [:orders 1 :order/outstanding-amount])
3 (catch clojure.lang.ExceptionInfo ex
4 (ex-data ex)))
Use structured exception data for broken assumptions, not for every expected domain result.
Not every unhappy path should be an exception.
| Situation | Better Clojure shape |
|---|---|
| Customer is not eligible | Return {:status :denied :reason ...}. |
| Input map is missing a required key | Throw ex-info or reject at boundary validation. |
| External service times out | Return or throw according to the orchestration policy, with context. |
| Business rule requires manual review | Return {:status :review :reason ...}. |
| Adapter cannot convert a Java enum | Fail fast with structured exception data. |
Expected business outcomes should usually be values. Broken system conditions should be visible failures.
Use the REPL to reduce the bug to one expression.
1(->> (:orders failing-input)
2 (map :order/outstanding-amount)
3 (remove nil?)
4 (reduce + 0M))
Then decide whether removing nil is correct. The REPL makes it easy to test a fix; code review still decides whether the fix matches the domain.
| REPL move | Use it for |
|---|---|
| Evaluate a subexpression | Find which transformation breaks. |
| Bind a captured fixture | Reproduce production-shaped data. |
| Inspect intermediate values | Confirm map keys, types, and sequence contents. |
| Try a smaller function | Avoid stepping through framework code. |
| Add a regression test | Preserve the discovered case. |
Do not leave the bug fix as REPL history. Convert the captured case into a test.
Debugging production migrations requires correlation, not print statements scattered everywhere.
| Log field | Why it helps |
|---|---|
| Request or job ID | Connect Java and Clojure logs. |
| Migration slice name | Know which adapter or feature flag path ran. |
| Input identifier | Reproduce the case without logging sensitive payloads. |
| Output status | Compare old and new behavior safely. |
| Exception data | Preserve structured context from ex-info. |
Use println locally when it is the fastest way to understand a pure function. Use structured logging and metrics for production behavior.
ex-info failure test.ex-info is useful when an exception needs structured context.