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.
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?”
| 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. |
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.
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. |
macroexpand-1 result into a REPL and understand it?~expr appear only where you intend runtime evaluation?# or gensym?If the expansion is too large to review, the macro is probably doing too much.