Learn a practical macro debugging workflow in Clojure using macroexpand, expansion inspection, REPL-driven testing, and phase-aware debugging so errors are traced to the right part of the pipeline.
Macro debugging: Figuring out whether a macro problem comes from the input form, the emitted expansion, or the runtime behavior of the expanded code.
Macros are hardest to debug when you treat them like runtime functions. They are easier to debug when you separate the pipeline into three questions:
Most macro bugs become manageable as soon as you stop asking all three questions at once.
The most useful first tools are still:
macroexpand-1macroexpandclojure.pprint/pprint1(macroexpand-1
2 '(unless (pos? balance)
3 (println "Balance is zero or negative")))
Use macroexpand-1 when you want the next expansion step. Use macroexpand when nested macro layers matter. Pretty-printing the result is often the difference between “this seems wrong” and “this binding is duplicated right here.”
If the expansion looks wrong, fix the macro. If the expansion looks right, the bug may now just be ordinary Clojure code inside the emitted form.
That distinction matters because a macro can be perfectly correct while the generated runtime code still fails due to:
The debugging approach should change once the expansion itself is sound.
When a macro misbehaves, simplify the caller form until the mistake becomes obvious:
Macros often fail because of one syntactic edge case, not because every usage is broken.
Many macro bugs come from:
These are much easier to see in the expanded form than in the original macro source.
Macro tests should usually cover two things:
If you test only runtime behavior, expansion mistakes can hide for too long. If you test only expansion, the generated code may still behave incorrectly when executed.
Macros benefit from REPL-driven development more than almost any other abstraction:
That short loop is often better than trying to understand a macro from static reading alone.
You may spend time on behavior caused by the wrong emitted form.
Sometimes macroexpand-1 is more useful because it isolates the first transformation.
Large caller forms hide small expansion mistakes.
Macros often break on optional clauses, nested forms, or symbol-shape edge cases.
Start with macroexpand-1, pretty-print the emitted form, and shrink the caller input until the wrong expansion is obvious. Once the expansion looks correct, debug the generated runtime code like any other Clojure. Test both expansion shape and runtime behavior. In Clojure, macro debugging becomes tractable the moment you keep phase boundaries explicit.