Browse Learn Clojure Foundations as a Java Developer

Debug Clojure Macros with Expansions

Learn a practical macro debugging workflow for Clojure: inspect expansions, move runtime work into functions, test edge cases, and compare generated code with the call site.

Debugging a macro means debugging two things: the code that builds the expansion and the code produced by that expansion. A runtime debugger only sees the second part. The macro author must inspect the generated form directly.

For Java engineers, the closest habit is reviewing generated source or bytecode. In Clojure, the review surface is usually macroexpand-1, macroexpand, and a small REPL experiment.

Start with the Expansion

1(defmacro when-present [value & body]
2  `(when (some? ~value)
3     ~@body))
4
5(macroexpand-1
6  '(when-present user
7     (println (:id user))))

The first question is not “what did the macro name promise?” It is “what code did the macro actually generate?”

Debugging Workflow

Step Purpose
Expand one level with macroexpand-1. Check your macro without expanding every nested core macro.
Expand fully with macroexpand when needed. See the lower-level form Clojure will compile.
Pretty-print the expansion. Make binding, evaluation, and nesting problems visible.
Move runtime work into functions. Keep the macro small and test most behavior normally.
Test with side-effecting expressions. Expose repeated evaluation and ordering mistakes.

Keep Runtime Logic Out of the Macro

The macro should usually arrange syntax; functions should do ordinary runtime work.

1(defn present? [value]
2  (some? value))
3
4(defmacro when-present [value & body]
5  `(when (present? ~value)
6     ~@body))

This shape is easier to test than a macro that embeds all logic in generated code. Unit tests can cover present?, while expansion tests can verify the macro’s call-site shape.

Test the Pitfalls Directly

Use intentionally suspicious inputs:

1(def counter (atom 0))
2
3(macroexpand-1
4  '(log-once (swap! counter inc)))

Then ask:

Suspicious input What it reveals
(swap! counter inc) Whether an argument is evaluated more than once.
A caller local named like an internal temporary. Whether the macro has a capture problem.
A body with multiple forms. Whether ~@body lands inside the intended wrapper.
A failing expression. Whether exceptions point to understandable generated code.

Macro Debugging Checklist

  1. Can you paste the macroexpand-1 result into a REPL and understand it?
  2. Does every ~expr appear only where you intend runtime evaluation?
  3. Are macro-owned locals generated with # or gensym?
  4. Is ordinary runtime behavior delegated to functions?
  5. Does the quiz of edge-case inputs include side effects, nils, and body forms?

If the expansion is too large to review, the macro is probably doing too much.

Knowledge Check

### What should you inspect first when a macro behaves unexpectedly? - [x] The form produced by `macroexpand-1`. - [ ] The Java classpath. - [ ] The JVM garbage collector logs. - [ ] The namespace docstring. > **Explanation:** Macro bugs often live in the generated code. `macroexpand-1` shows what your macro produced before nested expansions obscure the shape. ### Why move runtime logic into helper functions? - [x] Functions are easier to test and keep the macro focused on syntax. - [ ] Functions prevent macro expansion from happening. - [ ] Functions automatically eliminate every side effect. - [ ] Functions make Java reflection faster. > **Explanation:** A small macro plus ordinary functions is easier to test, debug, and review than a large generated block. ### Which test input is useful for finding repeated evaluation? - [x] A side-effecting expression such as `(swap! counter inc)`. - [ ] A namespace alias. - [ ] A docstring. - [ ] A Java package name. > **Explanation:** Side-effecting expressions reveal whether the expansion evaluates the same caller form more times than intended.
Revised on Saturday, May 23, 2026